Initialize

This commit is contained in:
2025-05-10 13:31:06 -07:00
commit 631d47ddf9
19 changed files with 1288 additions and 0 deletions

60
Models/AppSettings.cs Normal file
View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
namespace FileExposer.Models;
public record AppSettings(FileWatcherConfiguration FileWatcherConfiguration,
SyncConfiguration SyncConfiguration)
{
public static AppSettings Get(IConfigurationRoot configurationRoot)
{
AppSettings result;
#pragma warning disable IL3050, IL2026
FileWatcherConfiguration? fileWatcherConfiguration = configurationRoot.GetSection(nameof(FileWatcherConfiguration)).Get<FileWatcherConfiguration>();
SyncConfiguration? syncConfiguration = configurationRoot.GetSection(nameof(SyncConfiguration)).Get<SyncConfiguration>();
#pragma warning restore IL3050, IL2026
if (fileWatcherConfiguration is null
|| syncConfiguration is null
|| fileWatcherConfiguration?.Company is null)
{
List<string> paths = [];
foreach (IConfigurationProvider configurationProvider in configurationRoot.Providers)
{
if (configurationProvider is not Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider jsonConfigurationProvider)
continue;
if (jsonConfigurationProvider.Source.FileProvider is not Microsoft.Extensions.FileProviders.PhysicalFileProvider physicalFileProvider)
continue;
paths.Add(physicalFileProvider.Root);
}
throw new NotSupportedException($"Not found!{Environment.NewLine}{string.Join(Environment.NewLine, paths.Distinct())}");
}
result = new(fileWatcherConfiguration,
syncConfiguration);
Verify(result);
return result;
}
private static void Verify(AppSettings appSettings)
{
if (appSettings.FileWatcherConfiguration.MaxDegreeOfParallelism > Environment.ProcessorCount)
throw new Exception($"Environment.ProcessorCount must be larger or equal to {nameof(appSettings.FileWatcherConfiguration.MaxDegreeOfParallelism)}");
}
public override string ToString()
{
string result = JsonSerializer.Serialize(this, AppSettingsSourceGenerationContext.Default.AppSettings);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(AppSettings))]
internal partial class AppSettingsSourceGenerationContext : JsonSerializerContext
{
}

View File

@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FileExposer.Models;
public record FileWatcherConfiguration(string Company,
string Helper,
int MaxDegreeOfParallelism,
string UniformResourceLocator)
{
public override string ToString()
{
string result = JsonSerializer.Serialize(this, FileWatcherConfigurationSourceGenerationContext.Default.FileWatcherConfiguration);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(FileWatcherConfiguration))]
internal partial class FileWatcherConfigurationSourceGenerationContext : JsonSerializerContext
{
}

4
Models/Get.cs Normal file
View File

@ -0,0 +1,4 @@
namespace FileExposer.Models;
public record Get(string? JSON,
byte[]? Bytes);

View File

@ -0,0 +1,13 @@
namespace FileExposer.Models;
public interface ISyncV1Controller<T, TT>
{
static string GetRouteName() => nameof(ISyncV1Controller<T, TT>)[1..^10];
T Post();
T Put(string path, TT formFiles);
T Patch(string path, TT formFiles);
T Get(string path, long? size, long? ticks);
T Delete(string path, long size, long ticks);
}

View File

@ -0,0 +1,15 @@
using System;
using System.IO;
namespace FileExposer.Models;
public interface ISyncV1Repository
{
string Post(Stream stream);
Get Get(string path, long? size, long? ticks);
void Delete(string path, long size, long ticks);
void Put(string path, string data, Stream stream);
void Patch(string path, string data, Stream stream);
}

79
Models/Record.cs Normal file
View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using Microsoft.Extensions.FileSystemGlobbing;
namespace FileExposer.Models;
public record Record(string RelativePath,
long Size,
long Ticks)
{
internal static ReadOnlyCollection<Record> GetCollection(SyncConfiguration syncConfiguration, string rightDirectory)
{
ReadOnlyCollection<Record> results;
Matcher matcher = new();
string excludePatternsFile = Path.Combine(rightDirectory, syncConfiguration.ExcludePatternsFile);
string includePatternsFile = Path.Combine(rightDirectory, syncConfiguration.IncludePatternsFile);
matcher.AddIncludePatterns(!File.Exists(includePatternsFile) ? ["*"] : File.ReadAllLines(includePatternsFile));
matcher.AddExcludePatterns(!File.Exists(excludePatternsFile) ? ["System Volume Information"] : File.ReadAllLines(excludePatternsFile));
results = GetRecords(rightDirectory, matcher);
return results;
}
private static ReadOnlyCollection<Record> GetRecords(string rightDirectory, Matcher matcher)
{
List<Record> results = [];
Record record;
FileInfo fileInfo;
string relativePath;
ReadOnlyCollection<ReadOnlyCollection<string>> collection = GetFilesCollection(rightDirectory, "*", "*");
foreach (ReadOnlyCollection<string> c in collection)
{
foreach (string f in c)
{
if (!matcher.Match(rightDirectory, f).HasMatches)
continue;
fileInfo = new(f);
if (fileInfo.Length == 0)
continue;
relativePath = Path.GetRelativePath(rightDirectory, fileInfo.FullName);
record = new(RelativePath: relativePath,
Size: fileInfo.Length,
Ticks: fileInfo.LastWriteTime.ToUniversalTime().Ticks);
results.Add(record);
}
}
return results.AsReadOnly();
}
private static ReadOnlyCollection<ReadOnlyCollection<string>> GetFilesCollection(string directory, string directorySearchFilter, string fileSearchFilter)
{
List<ReadOnlyCollection<string>> results = [];
string[] files;
if (!fileSearchFilter.Contains('*'))
fileSearchFilter = string.Concat('*', fileSearchFilter);
if (!directorySearchFilter.Contains('*'))
directorySearchFilter = string.Concat('*', directorySearchFilter);
if (!Directory.Exists(directory))
_ = Directory.CreateDirectory(directory);
results.Add(Directory.GetFiles(directory, fileSearchFilter, SearchOption.TopDirectoryOnly).AsReadOnly());
string[] directories = Directory.GetDirectories(directory, directorySearchFilter, SearchOption.TopDirectoryOnly);
foreach (string innerDirectory in directories)
{
try
{
files = Directory.GetFiles(innerDirectory, fileSearchFilter, SearchOption.AllDirectories);
if (files.Length == 0)
continue;
results.Add(files.AsReadOnly());
}
catch (UnauthorizedAccessException)
{ continue; }
}
return results.AsReadOnly();
}
}

39
Models/RelativePath.cs Normal file
View File

@ -0,0 +1,39 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace FileExposer.Models;
public record RelativePath(string Path,
Record[] Records)
{
internal static RelativePath? Get(Stream stream)
{
RelativePath? result;
string? json = GetJson(stream);
result = string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize<RelativePath>(json);
return result;
}
private static string? GetJson(Stream stream)
{
string? result;
if (!stream.CanRead)
result = null;
else
{
Task<string> task = new StreamReader(stream).ReadToEndAsync();
result = task.Result;
}
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(RelativePath))]
public partial class RelativePathSourceGenerationContext : JsonSerializerContext
{
}

233
Models/Review.cs Normal file
View File

@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
namespace FileExposer.Models;
public record Review(Segment[]? AreEqual,
Segment[]? LeftSideIsNewer,
Segment[]? LeftSideOnly,
Segment[]? NotEqualBut,
Record[]? Records,
Segment[]? RightSideIsNewer,
Segment[]? RightSideOnly)
{
internal static Review Get(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
Review result;
ReadOnlyCollection<Segment> areEqual = GetAreEqual(relativePath, records);
ReadOnlyCollection<Segment> notEqualBut = GetNotEqualBut(relativePath, records);
ReadOnlyCollection<Segment> leftSideOnly = GetLeftSideOnly(relativePath, records);
ReadOnlyCollection<Segment> rightSideOnly = GetRightSideOnly(relativePath, records);
ReadOnlyCollection<Segment> leftSideIsNewer = GetLeftSideIsNewer(relativePath, records);
ReadOnlyCollection<Segment> rightSideIsNewer = GetRightSideIsNewer(relativePath, records);
result = new(AreEqual: [.. areEqual],
LeftSideIsNewer: [.. leftSideIsNewer],
LeftSideOnly: [.. leftSideOnly],
NotEqualBut: [.. notEqualBut],
Records: null,
RightSideIsNewer: [.. rightSideIsNewer],
RightSideOnly: [.. rightSideOnly]);
return result;
}
private static ReadOnlyCollection<Segment> GetAreEqual(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
List<Segment> results = [];
Record? record;
Segment segment;
double totalSeconds;
string? checkDirectory = null;
ReadOnlyDictionary<string, Record> keyValuePairs = GetKeyValuePairs(relativePath);
foreach (Record r in records)
{
if (checkDirectory is null && r.Size == 0 && r.Ticks == 0)
{
checkDirectory = r.RelativePath;
continue;
}
if (r.RelativePath == relativePath.Path)
continue;
if (!keyValuePairs.TryGetValue(r.RelativePath, out record))
continue;
totalSeconds = new TimeSpan(record.Ticks - r.Ticks).TotalSeconds;
if (record.Size != r.Size || totalSeconds is > 2 or < -2)
continue;
segment = new(Left: r,
LeftDirectory: checkDirectory,
Right: record,
RightDirectory: relativePath.Path);
results.Add(segment);
}
return results.AsReadOnly();
}
private static ReadOnlyDictionary<string, Record> GetKeyValuePairs(RelativePath relativePath) =>
GetKeyValuePairs(relativePath.Records.AsReadOnly());
private static ReadOnlyDictionary<string, Record> GetKeyValuePairs(ReadOnlyCollection<Record> records)
{
Dictionary<string, Record> results = [];
foreach (Record record in records)
results.Add(record.RelativePath, record);
return new(results);
}
private static ReadOnlyCollection<Segment> GetNotEqualBut(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
List<Segment> results = [];
Record? record;
Segment segment;
double totalSeconds;
string? checkDirectory = null;
ReadOnlyDictionary<string, Record> keyValuePairs = GetKeyValuePairs(relativePath);
foreach (Record r in records)
{
if (checkDirectory is null && r.Size == 0 && r.Ticks == 0)
{
checkDirectory = r.RelativePath;
continue;
}
if (r.RelativePath == relativePath.Path)
continue;
if (!keyValuePairs.TryGetValue(r.RelativePath, out record))
continue;
if (record.Size == r.Size)
continue;
totalSeconds = new TimeSpan(record.Ticks - r.Ticks).TotalSeconds;
if (totalSeconds is >= 2 or <= -2)
continue;
segment = new(Left: r,
LeftDirectory: checkDirectory,
Right: record,
RightDirectory: relativePath.Path);
results.Add(segment);
}
return results.AsReadOnly();
}
private static ReadOnlyCollection<Segment> GetLeftSideOnly(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
List<Segment> results = [];
Record? record;
Segment segment;
string? checkDirectory = null;
ReadOnlyDictionary<string, Record> keyValuePairs = GetKeyValuePairs(relativePath);
foreach (Record r in records)
{
if (checkDirectory is null && r.Size == 0 && r.Ticks == 0)
{
checkDirectory = r.RelativePath;
continue;
}
if (r.RelativePath == relativePath.Path)
continue;
if (keyValuePairs.TryGetValue(r.RelativePath, out record))
continue;
segment = new(Left: r,
LeftDirectory: checkDirectory,
Right: record,
RightDirectory: relativePath.Path);
results.Add(segment);
}
return results.AsReadOnly();
}
private static ReadOnlyCollection<Segment> GetRightSideOnly(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
List<Segment> results = [];
Record? record;
Segment segment;
string? checkDirectory = null;
ReadOnlyDictionary<string, Record> keyValuePairs = GetKeyValuePairs(records);
foreach (Record r in relativePath.Records)
{
if (checkDirectory is null && r.Size == 0 && r.Ticks == 0)
{
checkDirectory = r.RelativePath;
continue;
}
if (r.RelativePath == relativePath.Path)
continue;
if (keyValuePairs.TryGetValue(r.RelativePath, out record))
continue;
segment = new(Left: record,
LeftDirectory: null,
Right: r,
RightDirectory: relativePath.Path);
results.Add(segment);
}
return results.AsReadOnly();
}
private static ReadOnlyCollection<Segment> GetLeftSideIsNewer(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
List<Segment> results = [];
Record? record;
Segment segment;
double totalSeconds;
string? checkDirectory = null;
ReadOnlyDictionary<string, Record> keyValuePairs = GetKeyValuePairs(relativePath);
foreach (Record r in records)
{
if (checkDirectory is null && r.Size == 0 && r.Ticks == 0)
{
checkDirectory = r.RelativePath;
continue;
}
if (r.RelativePath == relativePath.Path)
continue;
if (!keyValuePairs.TryGetValue(r.RelativePath, out record))
continue;
totalSeconds = new TimeSpan(record.Ticks - r.Ticks).TotalSeconds;
if (totalSeconds is > -2)
continue;
segment = new(Left: r,
LeftDirectory: checkDirectory,
Right: record,
RightDirectory: relativePath.Path);
results.Add(segment);
}
return results.AsReadOnly();
}
private static ReadOnlyCollection<Segment> GetRightSideIsNewer(RelativePath relativePath, ReadOnlyCollection<Record> records)
{
List<Segment> results = [];
Record? record;
Segment segment;
double totalSeconds;
string? checkDirectory = null;
ReadOnlyDictionary<string, Record> keyValuePairs = GetKeyValuePairs(records);
foreach (Record r in relativePath.Records)
{
if (checkDirectory is null && r.Size == 0 && r.Ticks == 0)
{
checkDirectory = r.RelativePath;
continue;
}
if (r.RelativePath == relativePath.Path)
continue;
if (!keyValuePairs.TryGetValue(r.RelativePath, out record))
continue;
totalSeconds = new TimeSpan(record.Ticks - r.Ticks).TotalSeconds;
if (totalSeconds is > -2)
continue;
segment = new(Left: record,
LeftDirectory: null,
Right: r,
RightDirectory: relativePath.Path);
results.Add(segment);
}
return results.AsReadOnly();
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Review))]
public partial class ReviewSourceGenerationContext : JsonSerializerContext
{
}

14
Models/Segment.cs Normal file
View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace FileExposer.Models;
public record Segment(Record? Left,
string? LeftDirectory,
Record? Right,
string RightDirectory);
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Segment))]
public partial class SegmentSourceGenerationContext : JsonSerializerContext
{
}

View File

@ -0,0 +1,23 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FileExposer.Models;
public record SyncConfiguration(string ExcludePatternsFile,
string IncludePatternsFile,
int SecondThreshold)
{
public override string ToString()
{
string result = JsonSerializer.Serialize(this, SyncConfigurationSourceGenerationContext.Default.SyncConfiguration);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(SyncConfiguration))]
internal partial class SyncConfigurationSourceGenerationContext : JsonSerializerContext
{
}

View File

@ -0,0 +1,87 @@
namespace FileExposer.Models;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
public class SyncV1Repository(AppSettings appSettings) : ISyncV1Repository
{
private readonly string _RepositoryName = nameof(SyncV1Repository)[..^10];
private readonly AppSettings _AppSettings = appSettings;
void ISyncV1Repository.Delete(string path, long size, long ticks)
{
FileInfo fileInfo = Verify(path, size, ticks);
if (fileInfo.FullName != path)
throw new Exception("Path must match!");
File.Delete(path);
}
Get ISyncV1Repository.Get(string path, long? size, long? ticks)
{
Get result;
if (string.IsNullOrEmpty(path))
throw new Exception("Path mush be set!");
if (size is not null && ticks is not null)
{
FileInfo fileInfo = Verify(path, size.Value, ticks.Value);
result = new (JSON: null, Bytes: File.ReadAllBytes(fileInfo.FullName));
}
else
{
if (File.Exists(path))
throw new Exception("Must pass size and ticks when passing a file!");
string directory = Path.GetFullPath(path);
ReadOnlyCollection<Record> records = Record.GetCollection(_AppSettings.SyncConfiguration, directory);
RelativePath relativePath = new(Path: path, Records: [.. records]);
result = new (JSON: JsonSerializer.Serialize(relativePath, RelativePathSourceGenerationContext.Default.RelativePath), Bytes: null);
}
return result;
}
private FileInfo Verify(string path, long size, long ticks)
{
FileInfo fileInfo = new(path);
if (fileInfo.Length != size)
throw new Exception("Size must match!");
TimeSpan timeSpan = new(fileInfo.LastWriteTime.ToUniversalTime().Ticks - ticks);
if (Math.Abs(timeSpan.TotalSeconds) > _AppSettings.SyncConfiguration.SecondThreshold)
throw new Exception("Ticks must be within the threshold!");
return fileInfo;
}
string ISyncV1Repository.Post(Stream stream)
{
string result;
RelativePath? relativePath = RelativePath.Get(stream) ??
throw new MissingFieldException();
ReadOnlyCollection<Record> records = Record.GetCollection(_AppSettings.SyncConfiguration, relativePath.Path);
if (records.Count == 0)
throw new Exception("No source records");
Review review = Review.Get(relativePath, records);
result = JsonSerializer.Serialize(review, ReviewSourceGenerationContext.Default.Review);
return result;
}
void ISyncV1Repository.Patch(string path, string data, Stream stream)
{
string st = "d:/Tmp/phares/VisualStudioCode/z-include-patterns - Copy C.nsv";
FileStream fileStream = new(st, FileMode.Truncate);
Task task = stream.CopyToAsync(fileStream);
task.Wait();
fileStream.Dispose();
}
void ISyncV1Repository.Put(string path, string data, Stream stream)
{
string st = "d:/Tmp/phares/VisualStudioCode/z-include-patterns - Copy C.nsv";
FileStream fileStream = new(st, FileMode.CreateNew);
Task task = stream.CopyToAsync(fileStream);
task.Wait();
fileStream.Dispose();
}
}