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