Merge branch 'dev' of https://github.com/nsnail/dot into dev

This commit is contained in:
nsnail 2022-12-12 20:00:43 +08:00
commit b3f0347d54
10 changed files with 392 additions and 82 deletions

View File

@ -15,7 +15,7 @@ namespace Dot;
public static class AssemblyInfo public static class AssemblyInfo
{ {
private const string _VERSION = "1.1.5"; private const string _VERSION = "1.1.6";
public const string ASSEMBLY_COMPANY = "nsnail"; public const string ASSEMBLY_COMPANY = "nsnail";
public const string ASSEMBLY_COPYRIGHT = $"Copyright (c) 2022 {ASSEMBLY_COMPANY}"; public const string ASSEMBLY_COPYRIGHT = $"Copyright (c) 2022 {ASSEMBLY_COMPANY}";
public const string ASSEMBLY_FILE_VERSION = _VERSION; public const string ASSEMBLY_FILE_VERSION = _VERSION;

183
src/Get/Main.cs Normal file
View File

@ -0,0 +1,183 @@
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using NSExt.Extensions;
namespace Dot.Get;
[Description(nameof(Str.DownloadTool))]
[Localization(typeof(Str))]
public partial class Main : ToolBase<Option>
{
private const string _PART = "part";
/// <summary>
/// 给定一个路径(存在的目录,或者存在的目录+存在或不存在的文件)
/// </summary>
/// <param name="path">存在的目录,或者存在的目录+存在或不存在的文件</param>
/// <param name="file">要写入的文件名</param>
/// <returns>返回一个可写的文件完整路径</returns>
private static string BuildFilePath(string path, string file)
{
if (GetUseablePath(ref path))
// path 是一个存在的文件,已追加尾标
return path;
// ReSharper disable once InvertIf
if (Directory.Exists(path)) { //path 是一个存在的目录。
path = Path.Combine(path, file); // 构建文件路径
GetUseablePath(ref path); // 追加序号。
return path;
}
// path 是一个不存在的目录或者文件 ,视为不存在的文件
return path;
}
private static bool GetUseablePath(ref string path)
{
var dir = Path.GetDirectoryName(path);
var name = Path.GetFileNameWithoutExtension(path);
var ext = Path.GetExtension(path);
var ret = false;
for (var i = 1;; ++i) {
if (File.Exists(path)) {
path = Path.Combine(dir!, $"{name}({i}){ext}");
ret = true;
continue;
}
break;
}
return ret;
}
private static void MergeParts(string mainFilePath)
{
var files = Directory.GetFiles(Path.GetDirectoryName(mainFilePath)! //
, $"{Path.GetFileName(mainFilePath)}.*.{_PART}", SearchOption.TopDirectoryOnly)
.OrderBy(x => x)
.ToArray();
using var fs = File.Create(mainFilePath);
fs.SetLength(PartRegex().Match(files.Last()).Groups[1].Value.Int64());
foreach (var file in files) {
using var fsc = File.OpenRead(file);
fsc.CopyTo(fs);
fsc.Close();
File.Delete(file);
}
}
[GeneratedRegex($"(\\d+)\\.{_PART}")]
private static partial Regex PartRegex();
private void StreamCopy(Stream source, Stream dest, Action<int> rateHandle)
{
Span<byte> buf = stackalloc byte[Opt.BufferSize];
int read;
while ((read = source.Read(buf)) != 0) {
dest.Write(read == Opt.BufferSize ? buf : buf[..read]);
rateHandle(read);
}
}
private void WriteParts(HttpResponseMessage rsp, string mainFilePath //
, long startPos, long endPos //
, Action<int> rateHandle)
{
Span<byte> buf = stackalloc byte[Opt.BufferSize];
using var stream = rsp.Content.ReadAsStream();
int read;
var file = $"{mainFilePath}.{startPos}-{endPos}.{_PART}";
using var fs = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
while ((read = stream.Read(buf)) != 0) {
fs.Write(read == Opt.BufferSize ? buf : buf[..read]);
rateHandle(read);
}
}
protected override async Task Core()
{
using var http = new HttpClient();
string attachment = default;
long contentLength = default;
var table = new Table().AddColumn(Str.DataIdentification).AddColumn(Str.DataContent).AddRow("Url", Opt.Url);
await AnsiConsole.Status()
.AutoRefresh(true)
.Spinner(Spinner.Known.Default)
.StartAsync($"{Str.RequestMetaData}: {Opt.Url}", async _ => {
using var headRsp = await http.SendAsync(new HttpRequestMessage(HttpMethod.Head, Opt.Url));
using var content = headRsp.Content;
contentLength = content.Headers.ContentLength ?? 0;
attachment = content.Headers.ContentDisposition?.FileName ??
Opt.Url[(Opt.Url.LastIndexOf('/') + 1)..];
foreach (var kv in content.Headers)
table.AddRow(kv.Key, string.Join(Environment.NewLine, kv.Value));
});
AnsiConsole.Write(table);
var timer = DateTime.Now;
var mainFilePath = BuildFilePath(Opt.Output, attachment);
await AnsiConsole.Progress()
.Columns(new ProgressBarColumn() //
, new SpinnerColumn() //
, new DownloadedColumn() //
, new TransferSpeedColumn() //
, new PercentageColumn() //
, new TaskDescriptionColumn() //
, new RemainingTimeColumn() //
)
.StartAsync(async ctx => {
var tParent = ctx.AddTask($"{Str.TotalProgress} {Str.RemainingTime}:").IsIndeterminate();
if (contentLength == 0) //未知文件长度,单线程下载;
{
await using var nets = await http.GetStreamAsync(Opt.Url);
await using var fs
= new FileStream(mainFilePath, FileMode.CreateNew, FileAccess.Write
, FileShare.None);
tParent.MaxValue = Opt.BufferSize + 1; //由于文件长度未知, 进度条终点永远至为当前长度+1
StreamCopy(nets, fs, x => {
tParent.MaxValue += x;
tParent.Increment(x);
});
tParent.MaxValue = tParent.Value; // 写完了
tParent.IsIndeterminate(false);
tParent.StopTask();
}
else //已知文件长度,多线程下载:
{
tParent.IsIndeterminate(false);
tParent.MaxValue = contentLength;
var chunkSize = contentLength / Opt.ChunkNumbers;
Parallel.For(0, Opt.ChunkNumbers
, new ParallelOptions { MaxDegreeOfParallelism = Opt.MaxParallel } //
, i => {
var tChild = ctx.AddTask(
$"{Str.Thread}{i} {Str.RemainingTime}:", maxValue: chunkSize);
using var getReq = new HttpRequestMessage(HttpMethod.Get, Opt.Url);
var startPos = i * chunkSize;
var endPos = startPos + chunkSize - 1;
if (i == Opt.ChunkNumbers - 1) endPos += contentLength % chunkSize;
getReq.Headers.Range = new RangeHeaderValue(startPos, endPos);
// ReSharper disable once AccessToDisposedClosure
using var getRsp
= http.Send(getReq, HttpCompletionOption.ResponseHeadersRead);
WriteParts(getRsp, mainFilePath, startPos, endPos, x => {
tChild.Increment(x);
tParent.Increment(x);
});
});
MergeParts(mainFilePath);
}
});
AnsiConsole.MarkupLine(
$"{Str.DownloadCompleted}, {Str.ElapsedTime}: {DateTime.Now - timer}, {Str.FileSaveLocation}: {mainFilePath}");
}
}

36
src/Get/Option.cs Normal file
View File

@ -0,0 +1,36 @@
namespace Dot.Get;
public class Option : OptionBase
{
[CommandOption("-b|--buffer-size")]
[Description(nameof(Str.BufferSize))]
[Localization(typeof(Str))]
[DefaultValue(8)]
public int BufferSize { get; set; }
[CommandOption("-c|--chunk-number")]
[Description(nameof(Str.ChunkNumbers))]
[Localization(typeof(Str))]
[DefaultValue(5)]
public int ChunkNumbers { get; set; }
[CommandOption("-m|--max-parallel")]
[Description(nameof(Str.MaxParallel))]
[Localization(typeof(Str))]
[DefaultValue(5)]
public int MaxParallel { get; set; }
[CommandOption("-o|--output")]
[Description(nameof(Str.OutputPath))]
[Localization(typeof(Str))]
[DefaultValue(".")]
public string Output { get; set; }
[CommandArgument(0, "<url>")]
[Description(nameof(Str.Url))]
[Localization(typeof(Str))]
public string Url { get; set; }
}

View File

@ -24,35 +24,104 @@
<value>The input text is empty</value> <value>The input text is empty</value>
</data> </data>
<data name="SearchingFile" xml:space="preserve"> <data name="SearchingFile" xml:space="preserve">
<value>Find files...</value> <value>Find file</value>
</data> </data>
<data name="PathNotFound" xml:space="preserve"> <data name="PathNotFound" xml:space="preserve">
<value>The specified path "{0}" does not exist</value> <value>The specified path "{0}" does not exist</value>
</data> </data>
<data name="InvalidJsonString" xml:space="preserve">
<value>The clipboard does not contain the correct Json string</value>
</data>
<data name="SearchingFileOK" xml:space="preserve"> <data name="SearchingFileOK" xml:space="preserve">
<value>{0} files</value> <value>{0} files</value>
</data> </data>
<data name="ShowMessageTemp" xml:space="preserve"> <data name="ShowMessageTemp" xml:space="preserve">
<value>Read: {0}/{1}, processed: {2}, skipped: {3}</value> <value>Read: {0}/{1}, processed: {2}, skipped: {3}</value>
</data> </data>
<data name="TimeTool" xml:space="preserve">
<value>Time synchronization tool</value>
</data>
<data name="Ip" xml:space="preserve">
<value>IP tools</value>
</data>
<data name="Json" xml:space="preserve">
<value>Json tool</value>
</data>
<data name="ScreenPixelTool" xml:space="preserve">
<value>Screen coordinate color selection tool</value>
</data>
<data name="DownloadTool" xml:space="preserve">
<value>Multithreaded download tool</value>
</data>
<data name="TextTool" xml:space="preserve">
<value>Text encoding tool</value>
</data>
<data name="TextTobeProcessed" xml:space="preserve">
<value>Text to be processed (clipboard value is taken by default)</value>
</data>
<data name="Copied" xml:space="preserve"> <data name="Copied" xml:space="preserve">
<value>{0}(copied to clipboard)</value> <value>{0}(copied to clipboard)</value>
</data> </data>
<data name="GitOutputEncoding" xml:space="preserve">
<value>Git output encoding</value>
</data>
<data name="MaxRecursionDepth" xml:space="preserve">
<value>Directory search depth</value>
</data>
<data name="FileSearchPattern" xml:space="preserve"> <data name="FileSearchPattern" xml:space="preserve">
<value>File wildcards</value> <value>File wildcards</value>
</data> </data>
<data name="KeepSession" xml:space="preserve">
<value>Keep the session after executing the command</value>
</data>
<data name="TimeoutMillSecs" xml:space="preserve">
<value>Timeout for connecting to the NTP server (milliseconds)</value>
</data>
<data name="SyncToLocalTime" xml:space="preserve">
<value>Synchronize local time</value>
</data>
<data name="LocalClock" xml:space="preserve">
<value>Native clock</value>
</data>
<data name="ServerTime" xml:space="preserve">
<value>Synchronize local time</value>
</data>
<data name="FolderPath" xml:space="preserve"> <data name="FolderPath" xml:space="preserve">
<value>Directory path to be processed</value> <value>Directory path to be processed</value>
</data> </data>
<data name="OutputPath" xml:space="preserve">
<value>Output file path</value>
</data>
<data name="Url" xml:space="preserve">
<value>URL</value>
</data>
<data name="ConvertEndOfLineToLF" xml:space="preserve"> <data name="ConvertEndOfLineToLF" xml:space="preserve">
<value>Convert newline characters to LF</value> <value>Convert newline characters to LF</value>
</data> </data>
<data name="GuidTool" xml:space="preserve"> <data name="GuidTool" xml:space="preserve">
<value>GUID tool</value> <value>GUID tool</value>
</data> </data>
<data name="GitTool" xml:space="preserve">
<value>Git batch operation tool</value>
</data>
<data name="UseUppercase" xml:space="preserve"> <data name="UseUppercase" xml:space="preserve">
<value>Use uppercase output</value> <value>Use uppercase output</value>
</data> </data>
<data name="GitArgs" xml:space="preserve">
<value>Parameters passed to Git</value>
</data>
<data name="CompressJson" xml:space="preserve">
<value>Compress Json text</value>
</data>
<data name="FormatJson" xml:space="preserve">
<value>Format Json text</value>
</data>
<data name="GeneratorClass" xml:space="preserve">
<value>Generate entity class</value>
</data>
<data name="JsonToString" xml:space="preserve">
<value>Escape Json text into a string</value>
</data>
<data name="RandomPasswordGenerator" xml:space="preserve"> <data name="RandomPasswordGenerator" xml:space="preserve">
<value>Random password generator</value> <value>Random password generator</value>
</data> </data>
@ -68,20 +137,20 @@
<data name="TrimUtf8Bom" xml:space="preserve"> <data name="TrimUtf8Bom" xml:space="preserve">
<value>Remove the uf8 bom of the file</value> <value>Remove the uf8 bom of the file</value>
</data> </data>
<data name="TextTobeProcessed" xml:space="preserve">
<value>Text to be processed (clipboard value is taken by default)</value>
</data>
<data name="PressAnyKey" xml:space="preserve"> <data name="PressAnyKey" xml:space="preserve">
<value>Press any key to continue...</value> <value>Press any key to continue...</value>
</data> </data>
<data name="WriteMode" xml:space="preserve">
<value>Enable write mode</value>
</data>
<data name="NoFileToBeProcessed" xml:space="preserve"> <data name="NoFileToBeProcessed" xml:space="preserve">
<value>No documents to be processed</value> <value>No documents to be processed</value>
</data> </data>
<data name="TimeoutMillSecs" xml:space="preserve"> <data name="ExcludePathRegexes" xml:space="preserve">
<value>Timeout for connecting to the NTP server (milliseconds)</value> <value>Regular expressions for excluding paths</value>
</data> </data>
<data name="SyncToLocalTime" xml:space="preserve"> <data name="Exclude" xml:space="preserve">
<value>Synchronize local time</value> <value>exclude</value>
</data> </data>
<data name="NtpReceiveDone" xml:space="preserve"> <data name="NtpReceiveDone" xml:space="preserve">
<value>Success {0}/{1}, the average value of the clock offset of the machine:{2}ms</value> <value>Success {0}/{1}, the average value of the clock offset of the machine:{2}ms</value>
@ -95,6 +164,9 @@
<data name="LocalTimeOffset" xml:space="preserve"> <data name="LocalTimeOffset" xml:space="preserve">
<value>{0}, local clock offset: {1} ms</value> <value>{0}, local clock offset: {1} ms</value>
</data> </data>
<data name="NtpClock" xml:space="preserve">
<value>NTP standard clock</value>
</data>
<data name="LocalTimeSyncDone" xml:space="preserve"> <data name="LocalTimeSyncDone" xml:space="preserve">
<value>Local time has been synchronized</value> <value>Local time has been synchronized</value>
</data> </data>
@ -107,71 +179,17 @@
<data name="LocalClockOffset" xml:space="preserve"> <data name="LocalClockOffset" xml:space="preserve">
<value>Local clock offset</value> <value>Local clock offset</value>
</data> </data>
<data name="TimeTool" xml:space="preserve">
<value>Time synchronization tool</value>
</data>
<data name="TextTool" xml:space="preserve">
<value>Text encoding tool</value>
</data>
<data name="ScreenPixelTool" xml:space="preserve">
<value>Screen coordinate color selection tool</value>
</data>
<data name="ClickCopyColor" xml:space="preserve"> <data name="ClickCopyColor" xml:space="preserve">
<value>Click the left mouse button to copy the colors and coordinates to the clipboard</value> <value>Click the left mouse button to copy the colors and coordinates to the clipboard</value>
</data> </data>
<data name="PublicIP" xml:space="preserve"> <data name="PublicIP" xml:space="preserve">
<value>Public network ip: </value> <value>Public network ip... </value>
</data>
<data name="ServerTime" xml:space="preserve">
<value>Synchronize local time</value>
</data>
<data name="Ip" xml:space="preserve">
<value>IP tools</value>
</data>
<data name="KeepSession" xml:space="preserve">
<value>Keep the session after executing the command</value>
</data>
<data name="NtpClock" xml:space="preserve">
<value>NTP server standard clock: {0}</value>
</data>
<data name="GitTool" xml:space="preserve">
<value>Git batch operation tool</value>
</data>
<data name="GitArgs" xml:space="preserve">
<value>Parameters passed to Git</value>
</data> </data>
<data name="Ok" xml:space="preserve"> <data name="Ok" xml:space="preserve">
<value>OK</value> <value>OK</value>
</data> </data>
<data name="FindGitReps" xml:space="preserve"> <data name="FindGitReps" xml:space="preserve">
<value>Find all git repository directories under "{0}"...</value> <value>Find all git warehouse directories under "{0}" </value>
</data>
<data name="GitOutputEncoding" xml:space="preserve">
<value>Git output encoding</value>
</data>
<data name="MaxRecursionDepth" xml:space="preserve">
<value>Directory search depth</value>
</data>
<data name="InvalidJsonString" xml:space="preserve">
<value>Clipboard does not contain correct Json string</value>
</data>
<data name="Json" xml:space="preserve">
<value>JsonTools</value>
</data>
<data name="CompressJson" xml:space="preserve">
<value>Compress Json text</value>
</data>
<data name="FormatJson" xml:space="preserve">
<value>Format JSON text</value>
</data>
<data name="GeneratorClass" xml:space="preserve">
<value>generate entity classes</value>
</data>
<data name="JsonToString" xml:space="preserve">
<value>Json text escaped into a string</value>
</data>
<data name="WriteMode" xml:space="preserve">
<value>Enable write mode</value>
</data> </data>
<data name="ExerciseMode" xml:space="preserve"> <data name="ExerciseMode" xml:space="preserve">
<value>Read-only mode, the file will not be modified in real time!</value> <value>Read-only mode, the file will not be modified in real time!</value>
@ -188,20 +206,11 @@
<data name="WriteFileStats" xml:space="preserve"> <data name="WriteFileStats" xml:space="preserve">
<value>Write statistics</value> <value>Write statistics</value>
</data> </data>
<data name="LocalClock" xml:space="preserve">
<value>local clock</value>
</data>
<data name="ExcludePathRegexes" xml:space="preserve">
<value>Regular expression to exclude paths</value>
</data>
<data name="Exclude" xml:space="preserve">
<value>exclude</value>
</data>
<data name="Repository" xml:space="preserve"> <data name="Repository" xml:space="preserve">
<value>storehouse</value> <value>warehouse</value>
</data> </data>
<data name="Command" xml:space="preserve"> <data name="Command" xml:space="preserve">
<value>Order</value> <value>command</value>
</data> </data>
<data name="Response" xml:space="preserve"> <data name="Response" xml:space="preserve">
<value>response</value> <value>response</value>
@ -209,4 +218,40 @@
<data name="ZeroCode" xml:space="preserve"> <data name="ZeroCode" xml:space="preserve">
<value>Git exit code is zero</value> <value>Git exit code is zero</value>
</data> </data>
<data name="DataIdentification" xml:space="preserve">
<value>Data identification</value>
</data>
<data name="DataContent" xml:space="preserve">
<value>Data content</value>
</data>
<data name="RemainingTime" xml:space="preserve">
<value>Remaining time</value>
</data>
<data name="TotalProgress" xml:space="preserve">
<value>Total progress</value>
</data>
<data name="Thread" xml:space="preserve">
<value>thread</value>
</data>
<data name="DownloadCompleted" xml:space="preserve">
<value>Download complete</value>
</data>
<data name="ElapsedTime" xml:space="preserve">
<value>Cumulative time-consuming</value>
</data>
<data name="FileSaveLocation" xml:space="preserve">
<value>File save location</value>
</data>
<data name="RequestMetaData" xml:space="preserve">
<value>Request metadata</value>
</data>
<data name="ChunkNumbers" xml:space="preserve">
<value>Number of download blocks</value>
</data>
<data name="MaxParallel" xml:space="preserve">
<value>Maximum number of concurrency</value>
</data>
<data name="BufferSize" xml:space="preserve">
<value>Buffer size (kilobytes)</value>
</data>
</root> </root>

View File

@ -54,6 +54,9 @@
<data name="ScreenPixelTool" xml:space="preserve"> <data name="ScreenPixelTool" xml:space="preserve">
<value>屏幕坐标颜色选取工具</value> <value>屏幕坐标颜色选取工具</value>
</data> </data>
<data name="DownloadTool" xml:space="preserve">
<value>多线程下载工具</value>
</data>
<data name="TextTool" xml:space="preserve"> <data name="TextTool" xml:space="preserve">
<value>文本编码工具</value> <value>文本编码工具</value>
</data> </data>
@ -91,6 +94,12 @@
<data name="FolderPath" xml:space="preserve"> <data name="FolderPath" xml:space="preserve">
<value>要处理的目录路径</value> <value>要处理的目录路径</value>
</data> </data>
<data name="OutputPath" xml:space="preserve">
<value>输出文件路径</value>
</data>
<data name="Url" xml:space="preserve">
<value>URL</value>
</data>
<data name="ConvertEndOfLineToLF" xml:space="preserve"> <data name="ConvertEndOfLineToLF" xml:space="preserve">
<value>转换换行符为LF</value> <value>转换换行符为LF</value>
</data> </data>
@ -220,4 +229,40 @@
<data name="ZeroCode" xml:space="preserve"> <data name="ZeroCode" xml:space="preserve">
<value>Git退出码为零的</value> <value>Git退出码为零的</value>
</data> </data>
<data name="DataIdentification" xml:space="preserve">
<value>数据标识</value>
</data>
<data name="DataContent" xml:space="preserve">
<value>数据内容</value>
</data>
<data name="RemainingTime" xml:space="preserve">
<value>剩余时间</value>
</data>
<data name="TotalProgress" xml:space="preserve">
<value>总进度</value>
</data>
<data name="Thread" xml:space="preserve">
<value>线程</value>
</data>
<data name="DownloadCompleted" xml:space="preserve">
<value>下载完成</value>
</data>
<data name="ElapsedTime" xml:space="preserve">
<value>累计耗时</value>
</data>
<data name="FileSaveLocation" xml:space="preserve">
<value>文件保存位置</value>
</data>
<data name="RequestMetaData" xml:space="preserve">
<value>请求元数据</value>
</data>
<data name="ChunkNumbers" xml:space="preserve">
<value>下载分块数</value>
</data>
<data name="MaxParallel" xml:space="preserve">
<value>最大并发数量</value>
</data>
<data name="BufferSize" xml:space="preserve">
<value>缓冲区大小(千字节)</value>
</data>
</root> </root>

View File

@ -21,10 +21,11 @@ app.Configure(config => {
config.AddCommand<Dot.Text.Main>(nameof(Dot.Text).ToLower()); config.AddCommand<Dot.Text.Main>(nameof(Dot.Text).ToLower());
config.AddCommand<Dot.Time.Main>(nameof(Dot.Time).ToLower()); config.AddCommand<Dot.Time.Main>(nameof(Dot.Time).ToLower());
config.AddCommand<Dot.ToLf.Main>(nameof(Dot.ToLf).ToLower()); config.AddCommand<Dot.ToLf.Main>(nameof(Dot.ToLf).ToLower());
config.AddCommand<Dot.Get.Main>("get");
config.ValidateExamples(); config.ValidateExamples();
}); });
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return app.Run(args); return app.Run(args);

View File

@ -43,4 +43,4 @@ public sealed class Main : FilesTool<Option>
if (tmpFile != default) File.Delete(tmpFile); if (tmpFile != default) File.Delete(tmpFile);
} }
} }

View File

@ -1,3 +1,3 @@
namespace Dot.Rbom; namespace Dot.Rbom;
public class Option : DirOption { } public class Option : DirOption { }

View File

@ -20,4 +20,4 @@ public abstract class ToolBase<TOption> : Command<TOption> where TOption : Optio
AnsiConsole.Console.Input.ReadKey(true); AnsiConsole.Console.Input.ReadKey(true);
} }
} }
} }

View File

@ -50,4 +50,4 @@ public sealed class Main : FilesTool<Option>
ShowMessage(0, 1, 0); ShowMessage(0, 1, 0);
UpdateStats(Path.GetExtension(file)); UpdateStats(Path.GetExtension(file));
} }
} }