Files
romrepomgr/RomRepoMgr.Core/Filesystem/Vfs.cs

739 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using SharpCompress.Compressors;
using SharpCompress.Compressors.LZMA;
using ZstdSharp;
namespace RomRepoMgr.Core.Filesystem;
// TODO: Last handle goes negative
// TODO: Invalidate caches
// TODO: Mount options
// TODO: Do not show machines or romsets with no ROMs in repo
public class Vfs(ILoggerFactory loggerFactory) : IDisposable
{
readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, CachedDisk>> _machineDisksCache = [];
readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, CachedFile>> _machineFilesCache = [];
readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, CachedMedia>> _machineMediasCache = [];
readonly ConcurrentDictionary<long, ConcurrentDictionary<string, CachedMachine>> _machinesStatCache = [];
readonly ConcurrentDictionary<long, RomSet> _romSetsCache = [];
readonly ConcurrentDictionary<long, Stream> _streamsCache = [];
Fuse _fuse;
long _lastHandle;
ConcurrentDictionary<string, long> _rootDirectoryCache = [];
Winfsp _winfsp;
public static bool IsAvailable => OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()
? Fuse.IsAvailable
: OperatingSystem.IsWindows() && Winfsp.IsAvailable;
public void Dispose() => Umount();
public event EventHandler<System.EventArgs> Umounted;
public void MountTo(string mountPoint)
{
if((OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) && Fuse.IsAvailable)
{
_fuse = new Fuse(this)
{
MountPoint = mountPoint
};
_ = Task.Run(() =>
{
_fuse.Start();
CleanUp();
});
}
else if(OperatingSystem.IsWindows() && Winfsp.IsAvailable)
{
_winfsp = new Winfsp(this);
bool ret = _winfsp.Mount(mountPoint);
if(ret) return;
_winfsp = null;
CleanUp();
}
else
CleanUp();
}
public void Umount()
{
if(OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
_fuse?.Umount();
_fuse = null;
}
if(OperatingSystem.IsWindows())
{
_winfsp?.Umount();
_winfsp = null;
}
CleanUp();
}
void CleanUp()
{
foreach(KeyValuePair<long, Stream> handle in _streamsCache) handle.Value.Close();
_streamsCache.Clear();
_lastHandle = 0;
Umounted?.Invoke(this, System.EventArgs.Empty);
}
internal void GetInfo(out ulong files, out ulong totalSize)
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
totalSize = (ulong)(ctx.Files.Where(f => f.IsInRepo).Sum(f => (double)f.Size) +
ctx.Disks.Where(f => f.IsInRepo).Sum(f => (double)f.Size) +
ctx.Medias.Where(f => f.IsInRepo).Sum(f => (double)f.Size));
files = (ulong)(ctx.Files.Count(f => f.IsInRepo) +
ctx.Disks.Count(f => f.IsInRepo) +
ctx.Medias.Count(f => f.IsInRepo));
}
internal string[] SplitPath(string path) =>
path.Split(OperatingSystem.IsWindows() ? "\\" : "/", StringSplitOptions.RemoveEmptyEntries);
void FillRootDirectoryCache()
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
var rootCache = new ConcurrentDictionary<string, long>();
foreach(RomSet set in ctx.RomSets)
{
string name;
if(OperatingSystem.IsWindows())
{
name = set.Name.Replace('/', '')
.Replace('<', '\uFF1C')
.Replace('>', '\uFF1E')
.Replace(':', '\uFF1A')
.Replace('"', '\u2033')
.Replace('\\', '')
.Replace('|', '')
.Replace('?', '')
.Replace('*', '');
if(rootCache.ContainsKey(name))
{
name = Path.GetFileNameWithoutExtension(set.Filename)
?.Replace('/', '')
.Replace('<', '\uFF1C')
.Replace('>', '\uFF1E')
.Replace(':', '\uFF1A')
.Replace('"', '\u2033')
.Replace('\\', '')
.Replace('|', '')
.Replace('?', '')
.Replace('*', '');
}
}
else
{
name = set.Name.Replace('/', '');
if(rootCache.ContainsKey(name))
name = Path.GetFileNameWithoutExtension(set.Filename)?.Replace('/', '');
}
if(name == null || rootCache.ContainsKey(name)) name = Path.GetFileNameWithoutExtension(set.Sha384);
if(name == null) continue;
rootCache[name] = set.Id;
_romSetsCache[set.Id] = set;
}
_rootDirectoryCache = rootCache;
}
internal long GetRomSetId(string name)
{
if(_rootDirectoryCache.Count == 0) FillRootDirectoryCache();
if(!_rootDirectoryCache.TryGetValue(name, out long romSetId)) return -1;
return romSetId;
}
internal RomSet GetRomSet(long id)
{
if(_romSetsCache.TryGetValue(id, out RomSet romSet)) return romSet;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
romSet = ctx.RomSets.Find(id);
if(romSet == null) return null;
_romSetsCache[id] = romSet;
return romSet;
}
internal ConcurrentDictionary<string, CachedMachine> GetMachinesFromRomSet(long id)
{
_machinesStatCache.TryGetValue(id, out ConcurrentDictionary<string, CachedMachine> cachedMachines);
if(cachedMachines != null) return cachedMachines;
cachedMachines = [];
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
foreach(Machine mach in ctx.Machines.Where(m => m.RomSet.Id == id))
{
cachedMachines[mach.Name] = new CachedMachine
{
Id = mach.Id,
CreationDate = mach.CreatedOn,
ModificationDate = mach.UpdatedOn
};
}
_machinesStatCache[id] = cachedMachines;
return cachedMachines;
}
internal CachedMachine GetMachine(long romSetId, string name)
{
ConcurrentDictionary<string, CachedMachine> cachedMachines = GetMachinesFromRomSet(romSetId);
if(cachedMachines == null || !cachedMachines.TryGetValue(name, out CachedMachine machine)) return null;
return machine;
}
internal ConcurrentDictionary<string, CachedFile> GetFilesFromMachine(ulong id)
{
_machineFilesCache.TryGetValue(id, out ConcurrentDictionary<string, CachedFile> cachedMachineFiles);
if(cachedMachineFiles != null) return cachedMachineFiles;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
cachedMachineFiles = [];
foreach(FileByMachine machineFile in ctx.FilesByMachines.Where(fbm => fbm.Machine.Id == id && fbm.File.IsInRepo)
.Include(fileByMachine => fileByMachine.File))
{
var cachedFile = new CachedFile
{
Id = machineFile.File.Id,
Crc32 = machineFile.File.Crc32,
Md5 = machineFile.File.Md5,
Sha1 = machineFile.File.Sha1,
Sha256 = machineFile.File.Sha256,
Sha384 = machineFile.File.Sha384,
Sha512 = machineFile.File.Sha512,
Size = machineFile.File.Size,
CreatedOn = machineFile.File.CreatedOn,
UpdatedOn = machineFile.File.UpdatedOn,
FileLastModification = machineFile.FileLastModification
};
cachedMachineFiles[machineFile.Name] = cachedFile;
}
_machineFilesCache[id] = cachedMachineFiles;
return cachedMachineFiles;
}
internal ConcurrentDictionary<string, CachedDisk> GetDisksFromMachine(ulong id)
{
_machineDisksCache.TryGetValue(id, out ConcurrentDictionary<string, CachedDisk> cachedMachineDisks);
if(cachedMachineDisks != null) return cachedMachineDisks;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
cachedMachineDisks = [];
foreach(DiskByMachine machineDisk in ctx.DisksByMachines
.Where(dbm => dbm.Machine.Id == id &&
dbm.Disk.IsInRepo &&
dbm.Disk.Size != null)
.Include(diskByMachine => diskByMachine.Disk))
{
var cachedDisk = new CachedDisk
{
Id = machineDisk.Disk.Id,
Md5 = machineDisk.Disk.Md5,
Sha1 = machineDisk.Disk.Sha1,
Size = machineDisk.Disk.Size ?? 0,
CreatedOn = machineDisk.Disk.CreatedOn,
UpdatedOn = machineDisk.Disk.UpdatedOn
};
cachedMachineDisks[machineDisk.Name] = cachedDisk;
}
_machineDisksCache[id] = cachedMachineDisks;
return cachedMachineDisks;
}
internal ConcurrentDictionary<string, CachedMedia> GetMediasFromMachine(ulong id)
{
_machineMediasCache.TryGetValue(id, out ConcurrentDictionary<string, CachedMedia> cachedMachineMedias);
if(cachedMachineMedias != null) return cachedMachineMedias;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
cachedMachineMedias = [];
foreach(MediaByMachine machineMedia in ctx.MediasByMachines
.Where(mbm => mbm.Machine.Id == id &&
mbm.Media.IsInRepo &&
mbm.Media.Size != null)
.Include(mediaByMachine => mediaByMachine.Media))
{
var cachedDisk = new CachedMedia
{
Id = machineMedia.Media.Id,
Md5 = machineMedia.Media.Md5,
Sha1 = machineMedia.Media.Sha1,
Sha256 = machineMedia.Media.Sha256,
SpamSum = machineMedia.Media.SpamSum,
Size = machineMedia.Media.Size ?? 0,
CreatedOn = machineMedia.Media.CreatedOn,
UpdatedOn = machineMedia.Media.UpdatedOn
};
cachedMachineMedias[machineMedia.Name] = cachedDisk;
}
_machineMediasCache[id] = cachedMachineMedias;
return cachedMachineMedias;
}
internal CachedFile GetFile(ulong machineId, string name)
{
ConcurrentDictionary<string, CachedFile> cachedFiles = GetFilesFromMachine(machineId);
if(cachedFiles == null || !cachedFiles.TryGetValue(name, out CachedFile file)) return null;
return file;
}
internal CachedDisk GetDisk(ulong machineId, string name)
{
if(name.EndsWith(".chd", StringComparison.OrdinalIgnoreCase)) name = name[..^4];
ConcurrentDictionary<string, CachedDisk> cachedDisks = GetDisksFromMachine(machineId);
if(cachedDisks == null || !cachedDisks.TryGetValue(name, out CachedDisk disk)) return null;
return disk;
}
internal CachedMedia GetMedia(ulong machineId, string name)
{
if(name.EndsWith(".aif", StringComparison.OrdinalIgnoreCase)) name = name[..^4];
ConcurrentDictionary<string, CachedMedia> cachedMedias = GetMediasFromMachine(machineId);
if(cachedMedias == null || !cachedMedias.TryGetValue(name, out CachedMedia media)) return null;
return media;
}
internal long Open(string sha384, long fileSize)
{
byte[] sha384Bytes = new byte[48];
for(int i = 0; i < 48; i++)
{
if(sha384[i * 2] >= 0x30 && sha384[i * 2] <= 0x39)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x30) * 0x10);
else if(sha384[i * 2] >= 0x41 && sha384[i * 2] <= 0x46)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x37) * 0x10);
else if(sha384[i * 2] >= 0x61 && sha384[i * 2] <= 0x66)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x57) * 0x10);
if(sha384[i * 2 + 1] >= 0x30 && sha384[i * 2 + 1] <= 0x39)
sha384Bytes[i] += (byte)(sha384[i * 2 + 1] - 0x30);
else if(sha384[i * 2 + 1] >= 0x41 && sha384[i * 2 + 1] <= 0x46)
sha384Bytes[i] += (byte)(sha384[i * 2 + 1] - 0x37);
else if(sha384[i * 2 + 1] >= 0x61 && sha384[i * 2 + 1] <= 0x66)
sha384Bytes[i] += (byte)(sha384[i * 2 + 1] - 0x57);
}
string sha384B32 = Base32.ToBase32String(sha384Bytes);
string repoPath;
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32 + ".lz");
long handle;
if(!File.Exists(repoPath))
{
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32 + ".zst");
if(!File.Exists(repoPath)) return -1;
_lastHandle++;
handle = _lastHandle;
_streamsCache[handle] =
Stream.Synchronized(new ForcedSeekStream<DecompressionStream>(fileSize,
new FileStream(repoPath,
FileMode.Open,
FileAccess.Read)));
return handle;
}
_lastHandle++;
handle = _lastHandle;
_streamsCache[handle] =
Stream.Synchronized(new ForcedSeekStream<LZipStream>(fileSize,
new FileStream(repoPath,
FileMode.Open,
FileAccess.Read),
CompressionMode.Decompress));
return handle;
}
internal int Read(long handle, byte[] buf, long offset)
{
if(!_streamsCache.TryGetValue(handle, out Stream stream)) return -1;
using var rwLock = new ReaderWriterLockSlim();
try
{
rwLock.EnterReadLock();
stream.Position = offset;
return stream.EnsureRead(buf, 0, buf.Length);
}
finally
{
rwLock.ExitReadLock();
}
}
internal bool Close(long handle)
{
if(!_streamsCache.TryGetValue(handle, out Stream stream)) return false;
stream.Close();
_streamsCache.TryRemove(handle, out _);
return true;
}
internal IEnumerable<string> GetRootEntries()
{
if(_rootDirectoryCache.Count == 0) FillRootDirectoryCache();
return _rootDirectoryCache.Keys.ToArray();
}
public long OpenDisk(string sha1, string md5)
{
if(sha1 == null && md5 == null) return -1;
string repoPath = null;
string md5Path = null;
string sha1Path = null;
if(sha1 != null)
{
byte[] sha1Bytes = new byte[20];
for(int i = 0; i < 20; i++)
{
if(sha1[i * 2] >= 0x30 && sha1[i * 2] <= 0x39)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x30) * 0x10);
else if(sha1[i * 2] >= 0x41 && sha1[i * 2] <= 0x46)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x37) * 0x10);
else if(sha1[i * 2] >= 0x61 && sha1[i * 2] <= 0x66) sha1Bytes[i] = (byte)((sha1[i * 2] - 0x57) * 0x10);
if(sha1[i * 2 + 1] >= 0x30 && sha1[i * 2 + 1] <= 0x39)
sha1Bytes[i] += (byte)(sha1[i * 2 + 1] - 0x30);
else if(sha1[i * 2 + 1] >= 0x41 && sha1[i * 2 + 1] <= 0x46)
sha1Bytes[i] += (byte)(sha1[i * 2 + 1] - 0x37);
else if(sha1[i * 2 + 1] >= 0x61 && sha1[i * 2 + 1] <= 0x66)
sha1Bytes[i] += (byte)(sha1[i * 2 + 1] - 0x57);
}
string sha1B32 = Base32.ToBase32String(sha1Bytes);
sha1Path = Path.Combine(Settings.Settings.Current.RepositoryPath,
"chd",
"sha1",
sha1B32[0].ToString(),
sha1B32[1].ToString(),
sha1B32[2].ToString(),
sha1B32[3].ToString(),
sha1B32[4].ToString(),
sha1B32 + ".chd");
}
if(md5 != null)
{
byte[] md5Bytes = new byte[16];
for(int i = 0; i < 16; i++)
{
if(md5[i * 2] >= 0x30 && md5[i * 2] <= 0x39)
md5Bytes[i] = (byte)((md5[i * 2] - 0x30) * 0x10);
else if(md5[i * 2] >= 0x41 && md5[i * 2] <= 0x46)
md5Bytes[i] = (byte)((md5[i * 2] - 0x37) * 0x10);
else if(md5[i * 2] >= 0x61 && md5[i * 2] <= 0x66) md5Bytes[i] = (byte)((md5[i * 2] - 0x57) * 0x10);
if(md5[i * 2 + 1] >= 0x30 && md5[i * 2 + 1] <= 0x39)
md5Bytes[i] += (byte)(md5[i * 2 + 1] - 0x30);
else if(md5[i * 2 + 1] >= 0x41 && md5[i * 2 + 1] <= 0x46)
md5Bytes[i] += (byte)(md5[i * 2 + 1] - 0x37);
else if(md5[i * 2 + 1] >= 0x61 && md5[i * 2 + 1] <= 0x66) md5Bytes[i] += (byte)(md5[i * 2 + 1] - 0x57);
}
string md5B32 = Base32.ToBase32String(md5Bytes);
md5Path = Path.Combine(Settings.Settings.Current.RepositoryPath,
"chd",
"md5",
md5B32[0].ToString(),
md5B32[1].ToString(),
md5B32[2].ToString(),
md5B32[3].ToString(),
md5B32[4].ToString(),
md5B32 + ".chd");
}
if(File.Exists(sha1Path))
repoPath = sha1Path;
else if(File.Exists(md5Path)) repoPath = md5Path;
if(repoPath == null) return -1;
_lastHandle++;
long handle = _lastHandle;
_streamsCache[handle] = Stream.Synchronized(new FileStream(repoPath, FileMode.Open, FileAccess.Read));
return handle;
}
public long OpenMedia(string sha256, string sha1, string md5)
{
if(sha256 == null && sha1 == null && md5 == null) return -1;
string repoPath = null;
string md5Path = null;
string sha1Path = null;
string sha256Path = null;
if(sha256 != null)
{
byte[] sha256Bytes = new byte[32];
for(int i = 0; i < 32; i++)
{
if(sha256[i * 2] >= 0x30 && sha256[i * 2] <= 0x39)
sha256Bytes[i] = (byte)((sha256[i * 2] - 0x30) * 0x10);
else if(sha256[i * 2] >= 0x41 && sha256[i * 2] <= 0x46)
sha256Bytes[i] = (byte)((sha256[i * 2] - 0x37) * 0x10);
else if(sha256[i * 2] >= 0x61 && sha256[i * 2] <= 0x66)
sha256Bytes[i] = (byte)((sha256[i * 2] - 0x57) * 0x10);
if(sha256[i * 2 + 1] >= 0x30 && sha256[i * 2 + 1] <= 0x39)
sha256Bytes[i] += (byte)(sha256[i * 2 + 1] - 0x30);
else if(sha256[i * 2 + 1] >= 0x41 && sha256[i * 2 + 1] <= 0x46)
sha256Bytes[i] += (byte)(sha256[i * 2 + 1] - 0x37);
else if(sha256[i * 2 + 1] >= 0x61 && sha256[i * 2 + 1] <= 0x66)
sha256Bytes[i] += (byte)(sha256[i * 2 + 1] - 0x57);
}
string sha256B32 = Base32.ToBase32String(sha256Bytes);
sha256Path = Path.Combine(Settings.Settings.Current.RepositoryPath,
"aaru",
"sha256",
sha256B32[0].ToString(),
sha256B32[1].ToString(),
sha256B32[2].ToString(),
sha256B32[3].ToString(),
sha256B32[4].ToString(),
sha256B32 + ".aif");
}
if(sha1 != null)
{
byte[] sha1Bytes = new byte[20];
for(int i = 0; i < 20; i++)
{
if(sha1[i * 2] >= 0x30 && sha1[i * 2] <= 0x39)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x30) * 0x10);
else if(sha1[i * 2] >= 0x41 && sha1[i * 2] <= 0x46)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x37) * 0x10);
else if(sha1[i * 2] >= 0x61 && sha1[i * 2] <= 0x66) sha1Bytes[i] = (byte)((sha1[i * 2] - 0x57) * 0x10);
if(sha1[i * 2 + 1] >= 0x30 && sha1[i * 2 + 1] <= 0x39)
sha1Bytes[i] += (byte)(sha1[i * 2 + 1] - 0x30);
else if(sha1[i * 2 + 1] >= 0x41 && sha1[i * 2 + 1] <= 0x46)
sha1Bytes[i] += (byte)(sha1[i * 2 + 1] - 0x37);
else if(sha1[i * 2 + 1] >= 0x61 && sha1[i * 2 + 1] <= 0x66)
sha1Bytes[i] += (byte)(sha1[i * 2 + 1] - 0x57);
}
string sha1B32 = Base32.ToBase32String(sha1Bytes);
sha1Path = Path.Combine(Settings.Settings.Current.RepositoryPath,
"aaru",
"sha1",
sha1B32[0].ToString(),
sha1B32[1].ToString(),
sha1B32[2].ToString(),
sha1B32[3].ToString(),
sha1B32[4].ToString(),
sha1B32 + ".aif");
}
if(md5 != null)
{
byte[] md5Bytes = new byte[16];
for(int i = 0; i < 16; i++)
{
if(md5[i * 2] >= 0x30 && md5[i * 2] <= 0x39)
md5Bytes[i] = (byte)((md5[i * 2] - 0x30) * 0x10);
else if(md5[i * 2] >= 0x41 && md5[i * 2] <= 0x46)
md5Bytes[i] = (byte)((md5[i * 2] - 0x37) * 0x10);
else if(md5[i * 2] >= 0x61 && md5[i * 2] <= 0x66) md5Bytes[i] = (byte)((md5[i * 2] - 0x57) * 0x10);
if(md5[i * 2 + 1] >= 0x30 && md5[i * 2 + 1] <= 0x39)
md5Bytes[i] += (byte)(md5[i * 2 + 1] - 0x30);
else if(md5[i * 2 + 1] >= 0x41 && md5[i * 2 + 1] <= 0x46)
md5Bytes[i] += (byte)(md5[i * 2 + 1] - 0x37);
else if(md5[i * 2 + 1] >= 0x61 && md5[i * 2 + 1] <= 0x66) md5Bytes[i] += (byte)(md5[i * 2 + 1] - 0x57);
}
string md5B32 = Base32.ToBase32String(md5Bytes);
md5Path = Path.Combine(Settings.Settings.Current.RepositoryPath,
"aaru",
"md5",
md5B32[0].ToString(),
md5B32[1].ToString(),
md5B32[2].ToString(),
md5B32[3].ToString(),
md5B32[4].ToString(),
md5B32 + ".aif");
}
if(File.Exists(sha256Path))
repoPath = sha256Path;
else if(File.Exists(sha1Path))
repoPath = sha1Path;
else if(File.Exists(md5Path)) repoPath = md5Path;
if(repoPath == null) return -1;
_lastHandle++;
long handle = _lastHandle;
_streamsCache[handle] = Stream.Synchronized(new FileStream(repoPath, FileMode.Open, FileAccess.Read));
return handle;
}
[ContractInvariantMethod]
void ObjectInvariant()
{
Contract.Invariant(_machineMediasCache.All(pair => pair.Value != null));
}
}
internal sealed class CachedMachine
{
public ulong Id { get; set; }
public DateTime CreationDate { get; set; }
public DateTime ModificationDate { get; set; }
}
internal sealed class CachedFile
{
public ulong Id { get; set; }
public ulong Size { get; set; }
public string Crc32 { get; set; }
public string Md5 { get; set; }
public string Sha1 { get; set; }
public string Sha256 { get; set; }
public string Sha384 { get; set; }
public string Sha512 { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
public DateTime? FileLastModification { get; set; }
}
internal sealed class CachedDisk
{
public ulong Id { get; set; }
public ulong Size { get; set; }
public string Md5 { get; set; }
public string Sha1 { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
internal sealed class CachedMedia
{
public ulong Id { get; set; }
public ulong Size { get; set; }
public string Md5 { get; set; }
public string Sha1 { get; set; }
public string Sha256 { get; set; }
public string SpamSum { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}