diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/App.xaml b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/App.xaml new file mode 100644 index 0000000..638a851 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/App.xaml.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/App.xaml.cs new file mode 100644 index 0000000..bfe517e --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/App.xaml.cs @@ -0,0 +1,12 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace UsingHardLinkToZipNtfsDiskSize; +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ +} + diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/AssemblyInfo.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/ChannelLoggerProvider.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/ChannelLoggerProvider.cs new file mode 100644 index 0000000..1e3631d --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/ChannelLoggerProvider.cs @@ -0,0 +1,87 @@ +using System.Threading.Channels; +using Microsoft.Extensions.Logging; + +namespace UsingHardLinkToZipNtfsDiskSize; + +public class ChannelLoggerProvider : ILoggerProvider +{ + public ChannelLoggerProvider(params IStringLoggerWriter[] stringLoggerWriterList) + { + _stringLoggerWriterList = stringLoggerWriterList; + var channel = Channel.CreateUnbounded(new UnboundedChannelOptions() + { + SingleReader = true + }); + _channel = channel; + + Task.Run(WriteLogAsync); + } + + private readonly IStringLoggerWriter[] _stringLoggerWriterList; + + private async Task? WriteLogAsync() + { + while (!_channel.Reader.Completion.IsCompleted) + { + try + { + var message = await _channel.Reader.ReadAsync(); + foreach (var stringLoggerWriter in _stringLoggerWriterList) + { + await stringLoggerWriter.WriteAsync(message); + } + } + catch (ChannelClosedException) + { + // 结束 + } + } + + foreach (var stringLoggerWriter in _stringLoggerWriterList) + { + await stringLoggerWriter.DisposeAsync(); + } + } + + private readonly Channel _channel; + public void Dispose() + { + ChannelWriter channelWriter = _channel.Writer; + channelWriter.TryComplete(); + } + + public ILogger CreateLogger(string categoryName) + { + return new ChannelLogger(_channel.Writer); + } + + class ChannelLogger : ILogger, IDisposable + { + public ChannelLogger(ChannelWriter writer) + { + _writer = writer; + } + + private readonly ChannelWriter _writer; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var message = $"{DateTime.Now:yyyy.MM.dd HH:mm:ss,fff} [{logLevel}][{eventId}] {formatter(state, exception)}"; + _ = _writer.WriteAsync(message); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable? BeginScope(TState state) where TState : notnull + { + return this; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/DispatcherStringLoggerWriter.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/DispatcherStringLoggerWriter.cs new file mode 100644 index 0000000..f7ca05e --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/DispatcherStringLoggerWriter.cs @@ -0,0 +1,39 @@ +using System.Windows.Controls; + +namespace UsingHardLinkToZipNtfsDiskSize; + +class DispatcherStringLoggerWriter : IStringLoggerWriter +{ + public DispatcherStringLoggerWriter(TextBlock logTextBlock) + { + _logTextBlock = logTextBlock; + } + + private readonly TextBlock _logTextBlock; + + private string _lastMessage = string.Empty; + private bool _isInvalidate = false; + + public ValueTask WriteAsync(string message) + { + _lastMessage = message; + + if (!_isInvalidate) + { + _isInvalidate = true; + + _logTextBlock.Dispatcher.InvokeAsync(() => + { + _logTextBlock.Text = _lastMessage; + _isInvalidate = false; + }); + } + + return ValueTask.CompletedTask; + } + + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileRecordModel.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileRecordModel.cs new file mode 100644 index 0000000..ebba059 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileRecordModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace UsingHardLinkToZipNtfsDiskSize; + +public class FileRecordModel +{ + [Key] + [Required] + public string FilePath { set; get; } = null!; + + [Required] + public long FileLength { set; get; } + + [Required] + public string FileSha1Hash { set; get; } = null!; +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileStorageContext.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileStorageContext.cs new file mode 100644 index 0000000..8971e33 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileStorageContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace UsingHardLinkToZipNtfsDiskSize; + +public class FileStorageContext : DbContext +{ + public FileStorageContext() + { + // 用于设计时 + _sqliteFile = "FileManger.db"; + } + + public FileStorageContext(string sqliteFile) + { + _sqliteFile = sqliteFile; + } + + private readonly string _sqliteFile; + + public DbSet FileStorageModel { set; get; } = null!; + public DbSet FileRecordModel { set; get; } = null!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlite($"Filename={_sqliteFile}"); + } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileStorageModel.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileStorageModel.cs new file mode 100644 index 0000000..528ac2a --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/FileStorageModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace UsingHardLinkToZipNtfsDiskSize; + +public class FileStorageModel +{ + [Key] + [Required] + public string FileSha1Hash { set; get; } = null!; + + /// + /// 原始的文件路径 + /// + [Required] + public string OriginFilePath { set; get; } = null!; + + /// + /// 有多少个文件引用了 + /// + public long ReferenceCount { set; get; } + + /// + /// 文件大小 + /// + public long FileLength { set; get; } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/IStringLoggerWriter.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/IStringLoggerWriter.cs new file mode 100644 index 0000000..ebb892e --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/IStringLoggerWriter.cs @@ -0,0 +1,6 @@ +namespace UsingHardLinkToZipNtfsDiskSize; + +public interface IStringLoggerWriter : IAsyncDisposable +{ + ValueTask WriteAsync(string message); +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/LogFileStringLoggerWriter.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/LogFileStringLoggerWriter.cs new file mode 100644 index 0000000..0a4159a --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/LogFileStringLoggerWriter.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Text; + +namespace UsingHardLinkToZipNtfsDiskSize; + +public class LogFileStringLoggerWriter : IStringLoggerWriter +{ + public LogFileStringLoggerWriter(DirectoryInfo logFolder) + { + _logFolder = logFolder; + var logFile = Path.Join(logFolder.FullName, "Log.txt"); + var fileStream = new FileStream(logFile, FileMode.Append, FileAccess.Write, FileShare.Read); + _fileStream = fileStream; + var streamWriter = new StreamWriter(fileStream, Encoding.UTF8); + _streamWriter = streamWriter; + } + + private readonly DirectoryInfo _logFolder; + private readonly FileStream _fileStream; + private readonly StreamWriter _streamWriter; + + public async ValueTask WriteAsync(string message) + { + await _streamWriter.WriteLineAsync(message); + } + + public async ValueTask DisposeAsync() + { + await _streamWriter.DisposeAsync(); + await _fileStream.DisposeAsync(); + } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/MainWindow.xaml b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/MainWindow.xaml new file mode 100644 index 0000000..f538735 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/MainWindow.xaml @@ -0,0 +1,21 @@ + + + + + + + + + Drag Folder Here + + + + diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/MainWindow.xaml.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/MainWindow.xaml.cs new file mode 100644 index 0000000..d3399f1 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/MainWindow.xaml.cs @@ -0,0 +1,154 @@ +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Windows; +using Microsoft.Extensions.Logging; +using Path = System.IO.Path; +using Microsoft.EntityFrameworkCore; + +namespace UsingHardLinkToZipNtfsDiskSize; + +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } + + private void Grid_OnDragEnter(object sender, DragEventArgs e) + { + var data = e.Data.GetData(DataFormats.FileDrop) as string[]; + if (data != null && data.Length > 0) + { + var folder = data[0]; + if (Directory.Exists(folder)) + { + _ = StartUsingHardLinkToZipNtfsDiskSize(folder); + } + } + } + + private async ValueTask StartUsingHardLinkToZipNtfsDiskSize(string folder) + { + // 转换为日志存储文件夹 + var hexString = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(folder))); + + var logFolder = Path.GetFullPath(Path.Join(hexString, $"Log_{DateTime.Now:yyyy.MM.dd}")); + Directory.CreateDirectory(logFolder); + + var logFileStringLoggerWriter = new LogFileStringLoggerWriter(new DirectoryInfo(logFolder)); + var dispatcherStringLoggerWriter = new DispatcherStringLoggerWriter(LogTextBlock); + using var channelLoggerProvider = new ChannelLoggerProvider(logFileStringLoggerWriter, dispatcherStringLoggerWriter); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "yyyy.MM.dd HH:mm:ss "; + }); + // ReSharper disable once AccessToDisposedClosure + builder.AddProvider(channelLoggerProvider); + }); + + var sqliteFile = new FileInfo(Path.Join(hexString, "FileManger.db")); + + var logger = loggerFactory.CreateLogger(""); + + logger.LogInformation($"Start zip {folder} folder. LogFolder={logFolder}"); + + await Task.Run(async () => + { + var provider = new UsingHardLinkToZipNtfsDiskSizeProvider(); + await provider.Start(new DirectoryInfo(folder), sqliteFile, logger); + }); + } + + /// + /// 损伤修复措施 + /// + /// + /// + /// + /// + /// + /// 暂时不需要调用,因为损伤是代码逻辑写错,后续修复代码逻辑就用不到损伤修复。但是代码放着,也许后面依然有其他诡异的问题,可以继续修复 + private static async Task FixBreakFile(string folder, ILogger logger, string logFolder, string sqliteFile) + { + await Task.Run(async () => + { + logger.LogInformation($"开始损伤修复 {folder} 文件夹 LogFolder={logFolder}"); + + try + { + await using (var fileStorageContext = new FileStorageContext(sqliteFile)) + { + logger.LogInformation($"开始打开数据库文件 {sqliteFile}"); + + // 查找所有的记录文件 + await foreach (var fileRecordModel in fileStorageContext.FileRecordModel) + { + var filePath = fileRecordModel.FilePath; + logger.LogInformation($"开始 {filePath}"); + var fileExists = File.Exists(filePath); + logger.LogInformation($"判断 {filePath} 文件存在:{fileExists}"); + + if (!fileExists) + { + // 文件误删 + logger.LogInformation($"文件误删 {filePath} 尝试执行修复逻辑"); + + var success = false; + + foreach (var recordModel in fileStorageContext.FileRecordModel.Where(t => + t.FileSha1Hash == fileRecordModel.FileSha1Hash)) + { + var fixFileExists = File.Exists(recordModel.FilePath); + + logger.LogInformation( + $"SHA1={fileRecordModel.FileSha1Hash} 找到相近文件 {recordModel.FilePath} 修复的文件存在:{fixFileExists}"); + + if (fixFileExists) + { + logger.LogInformation($"准备拷贝文件修复 {recordModel.FilePath} 到 {filePath}"); + var result = UsingHardLinkToZipNtfsDiskSizeProvider.CreateHardLink(filePath, + recordModel.FilePath, logger); + + logger.LogInformation($"完成拷贝文件修复 结果={result} {recordModel.FilePath} 到 {filePath}"); + + success = File.Exists(filePath); + + if (!success) + { + logger.LogInformation($"修复失败!拷贝之后依然不存在文件"); + } + + break; + } + } + + if (success) + { + logger.LogInformation($"文件误删 {filePath} 修复成功"); + } + else + { + logger.LogInformation($"文件误删 {filePath} 修复失败,没有找到相似且存在的文件"); + } + + //Dispatcher.Invoke(() => MessageBox.Show($"修复 {filePath}")); + } + } + } + } + catch (Exception e) + { + logger.LogInformation($"执行失败 {e}"); + } + + logger.LogInformation("执行完成"); + }); + } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/20231120092807_Lindexi.Designer.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/20231120092807_Lindexi.Designer.cs new file mode 100644 index 0000000..1cfd873 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/20231120092807_Lindexi.Designer.cs @@ -0,0 +1,61 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using UsingHardLinkToZipNtfsDiskSize; + +#nullable disable + +namespace UsingHardLinkToZipNtfsDiskSize.Migrations +{ + [DbContext(typeof(FileStorageContext))] + [Migration("20231120092807_Lindexi")] + partial class Lindexi + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); + + modelBuilder.Entity("UsingHardLinkToZipNtfsDiskSize.FileRecordModel", b => + { + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("FileLength") + .HasColumnType("INTEGER"); + + b.Property("FileSha1Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("FilePath"); + + b.ToTable("FileRecordModel"); + }); + + modelBuilder.Entity("UsingHardLinkToZipNtfsDiskSize.FileStorageModel", b => + { + b.Property("FileSha1Hash") + .HasColumnType("TEXT"); + + b.Property("FileLength") + .HasColumnType("INTEGER"); + + b.Property("OriginFilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReferenceCount") + .HasColumnType("INTEGER"); + + b.HasKey("FileSha1Hash"); + + b.ToTable("FileStorageModel"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/20231120092807_Lindexi.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/20231120092807_Lindexi.cs new file mode 100644 index 0000000..6cdffc0 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/20231120092807_Lindexi.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UsingHardLinkToZipNtfsDiskSize.Migrations +{ + /// + public partial class Lindexi : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "FileRecordModel", + columns: table => new + { + FilePath = table.Column(type: "TEXT", nullable: false), + FileLength = table.Column(type: "INTEGER", nullable: false), + FileSha1Hash = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FileRecordModel", x => x.FilePath); + }); + + migrationBuilder.CreateTable( + name: "FileStorageModel", + columns: table => new + { + FileSha1Hash = table.Column(type: "TEXT", nullable: false), + OriginFilePath = table.Column(type: "TEXT", nullable: false), + ReferenceCount = table.Column(type: "INTEGER", nullable: false), + FileLength = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FileStorageModel", x => x.FileSha1Hash); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FileRecordModel"); + + migrationBuilder.DropTable( + name: "FileStorageModel"); + } + } +} diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/FileStorageContextModelSnapshot.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/FileStorageContextModelSnapshot.cs new file mode 100644 index 0000000..d021480 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/Migrations/FileStorageContextModelSnapshot.cs @@ -0,0 +1,58 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using UsingHardLinkToZipNtfsDiskSize; + +#nullable disable + +namespace UsingHardLinkToZipNtfsDiskSize.Migrations +{ + [DbContext(typeof(FileStorageContext))] + partial class FileStorageContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); + + modelBuilder.Entity("UsingHardLinkToZipNtfsDiskSize.FileRecordModel", b => + { + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("FileLength") + .HasColumnType("INTEGER"); + + b.Property("FileSha1Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("FilePath"); + + b.ToTable("FileRecordModel"); + }); + + modelBuilder.Entity("UsingHardLinkToZipNtfsDiskSize.FileStorageModel", b => + { + b.Property("FileSha1Hash") + .HasColumnType("TEXT"); + + b.Property("FileLength") + .HasColumnType("INTEGER"); + + b.Property("OriginFilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReferenceCount") + .HasColumnType("INTEGER"); + + b.HasKey("FileSha1Hash"); + + b.ToTable("FileStorageModel"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/NativeMethods.txt b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/NativeMethods.txt new file mode 100644 index 0000000..89f8e98 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/NativeMethods.txt @@ -0,0 +1 @@ +CreateHardLinkW \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UnitConverter.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UnitConverter.cs new file mode 100644 index 0000000..d2335f0 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UnitConverter.cs @@ -0,0 +1,70 @@ +namespace UsingHardLinkToZipNtfsDiskSize; + +public static class UnitConverter +{ + /// + /// 将文件大小转换为用于显示的大小 + /// + /// 文件大小,单位:B + /// 是否四舍五入 + /// 数字和单位间的分隔符 + /// 用于显示的字符串,只保留GB和TB的小数 + public static string ConvertSize(long value, bool isRound = true, string separators = "") + { + var (result, unit) = ConvertInner(value, 1024, 1, isRound, separators, DefaultSizeUnits); + + //只保留GB和TB的小数 + if (unit == "GB" || unit == "TB") + { + return $"{result}{unit}"; + } + else + { + // 其他,例如 MB 单位,不保留小数点。用 int 的强转去掉小数点,不能用 `ToString("0")` 的方式,因为此方式默认调用四舍五入的方式 + return $"{(int) result}{unit}"; + } + } + + private static readonly string[] DefaultSizeUnits = new string[] { "B", "KB", "MB", "GB", "TB" }; + + /// + /// 将数值转换为用于显示的数值 + /// + /// 需要转换的值 + /// 进制 + /// 小数位数 + /// 是否四舍五入 + /// 数字和单位间的分隔符 + /// 单位 + /// + /// - result 数值结果 + /// + /// - unit 单位 + /// + private static (double result, string unit) ConvertInner(long value, long interval, int digits, bool isRound, + string separators, string[] units) + { + var current = 0; + var temp = value; + + while (current < units.Length - 1 && temp >= interval) + { + current++; + temp /= interval; + } + var result = value * 1.0 / Math.Pow(interval, current); + + if (!isRound) + { + var pow = Math.Pow(10, digits); + return (Math.Truncate(result * pow) / pow, units[current]); + } + + if (string.IsNullOrEmpty(separators)) + { + return (Math.Round(result, digits), units[current]); + } + + return (Math.Round(result, digits), $"{separators}{units[current]}"); + } +} \ No newline at end of file diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UsingHardLinkToZipNtfsDiskSize.csproj b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UsingHardLinkToZipNtfsDiskSize.csproj new file mode 100644 index 0000000..1415095 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UsingHardLinkToZipNtfsDiskSize.csproj @@ -0,0 +1,30 @@ + + + + WinExe + net7.0-windows + enable + enable + true + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UsingHardLinkToZipNtfsDiskSizeProvider.cs b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UsingHardLinkToZipNtfsDiskSizeProvider.cs new file mode 100644 index 0000000..000fe94 --- /dev/null +++ b/CopyAfterCompileTool/UsingHardLinkToZipNtfsDiskSize/UsingHardLinkToZipNtfsDiskSizeProvider.cs @@ -0,0 +1,173 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Windows.Win32; +using Windows.Win32.Security; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; + +namespace UsingHardLinkToZipNtfsDiskSize; + +public class UsingHardLinkToZipNtfsDiskSizeProvider +{ + /// + /// 开始将 文件夹里面重复的文件使用硬连接压缩磁盘空间 + /// + /// + /// + /// + /// + public async Task Start(DirectoryInfo workFolder, FileInfo sqliteFile, ILogger logger) + { + await using (var fileStorageContext = new FileStorageContext(sqliteFile.FullName)) + { + await fileStorageContext.Database.MigrateAsync(); + } + + var destination = new byte[1024]; + long saveSize = 0; + long count = 0; + foreach (var file in workFolder.EnumerateFiles("*", enumerationOptions: new EnumerationOptions() + { + RecurseSubdirectories = true, + MaxRecursionDepth = 100, + })) + { + logger.LogInformation($"Start 第 {count} 个文件 {file}"); + count++; + + try + { + await using var fileStorageContext = new FileStorageContext(sqliteFile.FullName); + + long fileLength = file.Length; + + var fileRecordModel = await fileStorageContext.FileRecordModel.FindAsync(file.FullName); + if (fileRecordModel != null) + { + if (fileRecordModel.FileLength == fileLength) + { + // 上次压缩过了,不要重复处理 + continue; + } + } + + string sha1; + await using (var fileStream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var length = await SHA1.HashDataAsync(fileStream, destination); + sha1 = Convert.ToHexString(destination, 0, length); + } + + fileStorageContext.FileRecordModel.Add(new FileRecordModel() + { + FilePath = file.FullName, + FileLength = fileLength, + FileSha1Hash = sha1, + }); + + var fileStorageModel = await fileStorageContext.FileStorageModel.FindAsync(sha1); + if (fileStorageModel != null) + { + if (fileStorageModel.FileLength != fileLength) + { + logger.LogInformation($"SHA1={sha1} `fileStorageModel.FileLength != fileLength` fileStorageModel.FileLength={fileStorageModel.FileLength} fileLength={fileLength} fileStorageModel.OriginFilePath={fileStorageModel.OriginFilePath} file={file.FullName} 文件尺寸不匹配,不执行逻辑"); + continue; + } + + if (CreateHardLink(file.FullName, fileStorageModel.OriginFilePath, logger)) + { + // 省的空间 + saveSize += fileLength; + logger.LogInformation($"Exists Record SHA1={sha1} {file} FileSize={UnitConverter.ConvertSize(fileLength, separators: " ")} SaveSize:{UnitConverter.ConvertSize(saveSize, separators: " ")}"); + + fileStorageModel.ReferenceCount++; + fileStorageContext.FileStorageModel.Update(fileStorageModel); + } + else + { + // 拷贝失败的情况,可能是超过 Win32 限制数量 + // > The maximum number of hard links that can be created with this function is 1023 per file. If more than 1023 links are created for a file, an error results. + // 此时换成新的文件记录即可修复此问题 + fileStorageModel.OriginFilePath = file.FullName; + fileStorageContext.FileStorageModel.Update(fileStorageModel); + } + + if (!File.Exists(file.FullName)) + { + logger.LogInformation($"Error Break File {file.FullName} 文件损坏,文件找不到"); + } + } + else + { + fileStorageModel = new FileStorageModel() + { + FileLength = fileLength, + FileSha1Hash = sha1, + OriginFilePath = file.FullName, + ReferenceCount = 1 + }; + fileStorageContext.FileStorageModel.Add(fileStorageModel); + + logger.LogInformation($"Not exists Record {file} SHA1={sha1}"); + } + + await fileStorageContext.SaveChangesAsync(); + } + catch (Exception e) + { + logger.LogWarning($"Hard link fail {file} {e}"); + } + } + + logger.LogInformation($"Total save disk size: {UnitConverter.ConvertSize(saveSize, separators: " ")}"); + } + + /// + /// 创建硬连接 + /// + /// + /// + /// + /// 返回 false 表示创建失败 + public static bool CreateHardLink(string file, string originFilePath, ILogger logger) + { + if (file == originFilePath) + { + logger.LogInformation($"[CreateHardLink] 传入的原始文件相同,返回 false 啥都不做"); + return false; + } + + if (!File.Exists(originFilePath)) + { + logger.LogInformation($"[CreateHardLink] 传入的 originFilePath={originFilePath} 文件不存在"); + return false; + } + + if (File.Exists(file)) + { + logger.LogInformation($"[CreateHardLink] 传入的文件 file={file} 存在,正在删除"); + File.Delete(file); + } + + var lpSecurityAttributes = new SECURITY_ATTRIBUTES(); + var result = PInvoke.CreateHardLink(file, originFilePath, ref lpSecurityAttributes); + logger.LogInformation($"[CreateHardLink] PInvoke 结果={result == true} file={file} originFilePath={originFilePath}"); + + if (!result) + { + // 以下三个都获取不正确错误号 + // 如 An attempt was made to create more links on a file than the file system supports. + var lastWin32Error = Marshal.GetLastWin32Error(); + logger.LogInformation($"[CreateHardLink] PInvoke 结果={result == true} LastWin32Error={lastWin32Error} LastPInvokeError={Marshal.GetLastPInvokeError()} LastSystemError={Marshal.GetLastSystemError()}"); + } + + if (!File.Exists(file)) + { + logger.LogInformation($"[CreateHardLink] 创建符号链接失败,只好复制文件。 Copy {originFilePath} to {file}"); + File.Copy(originFilePath, file); + } + + return result; + } +} \ No newline at end of file