Files
Radioo_LincleLINK/LincleLINK/Logic/MainWindowLogic.cs
T

830 lines
32 KiB
C#

using BencodeNET.Parsing;
using BencodeNET.Torrents;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
namespace LincleLINK
{
public class MainWindowLogic : ViewModelBase
{
public static readonly string currentDir = Directory.GetCurrentDirectory();
//public static readonly string currentDir = @"F:\LincleLINK";
//public static readonly string currentDir = @"F:\LinkTool";
public static readonly string dbDir = Path.Join(currentDir, "db");
public static readonly string instanceDir = Path.Join(currentDir, "instance");
private ObservableCollection<InstanceListEntry> _instanceList;
public ObservableCollection<InstanceListEntry> InstanceList
{
get { return _instanceList; }
set
{
_instanceList = value;
OnPropertyChanged(nameof(InstanceList));
}
}
private InstanceListEntry _selectedInstance;
public InstanceListEntry SelectedInstance
{
get { return _selectedInstance; }
set
{
_selectedInstance = value;
OnPropertyChanged(nameof(SelectedInstance));
LogList.Add($"Selected {SelectedInstance.InstanceName}");
}
}
private ObservableCollection<string> _logList;
public ObservableCollection<string> LogList
{
get { return _logList; }
set
{
_logList = value;
Controls.LogScrollViewer.ScrollToBottom();
}
}
public double ProgressStep;
private double _progress;
public double Progress
{
get { return _progress; }
set
{
_progress = value;
OnPropertyChanged(nameof(Progress));
}
}
private bool _isFree;
public bool IsFree
{
get { return _isFree; }
set
{
_isFree = value;
OnPropertyChanged(nameof(IsFree));
}
}
private List<string> _matchedList;
public List<string> MatchedList
{
get { return _matchedList; }
set
{
_matchedList = value;
OnPropertyChanged(nameof(MatchedList));
Controls.MatchedFilesScroller.ScrollToBottom();
}
}
private string _relativePath;
public string RelativePath
{
get { return _relativePath; }
set
{
_relativePath = value;
OnPropertyChanged(nameof(RelativePath));
}
}
private string _torrentLinkTarget;
public string TorrentLinkTarget
{
get { return _torrentLinkTarget; }
set
{
_torrentLinkTarget = value;
OnPropertyChanged(nameof(TorrentLinkTarget));
}
}
private string _torrentFilePath;
public string TorrentFilePath
{
get { return _torrentFilePath; }
set
{
_torrentFilePath = value;
OnPropertyChanged(nameof(TorrentFilePath));
}
}
private string _torrentDownloadPath;
public string TorrentDownloadPath
{
get { return _torrentDownloadPath; }
set
{
_torrentDownloadPath = value;
OnPropertyChanged(nameof(TorrentDownloadPath));
}
}
private bool _didCheckFiles;
public bool DidCheckFiles
{
get { return _didCheckFiles; }
set
{
_didCheckFiles = value;
OnPropertyChanged(nameof(DidCheckFiles));
}
}
private bool _didCheckPieces;
public bool DidCheckPieces
{
get { return _didCheckPieces; }
set
{
_didCheckPieces = value;
OnPropertyChanged(nameof(DidCheckPieces));
}
}
private string _dbSize;
public string DBSize
{
get { return _dbSize; }
set
{
_dbSize = value;
OnPropertyChanged(nameof(DBSize));
}
}
private string _savings;
public string Savings
{
get { return _savings; }
set
{
_savings = value;
OnPropertyChanged(nameof(Savings));
}
}
private string _freeSpace;
public string FreeSpace
{
get { return _freeSpace; }
set
{
_freeSpace = value;
OnPropertyChanged(nameof(FreeSpace));
}
}
public SynchronizationContext UIContext { get; set; }
public Dictionary<PassedFileInfo, HashSet<long>>? FilePieceMap { get; set; }
public List<long>? BadPieces { get; set; }
public MainWindowControls Controls { get; set; }
public RelayCommand OpenAddInstanceCommand { get; set; }
public RelayCommand DeleteInstanceCommand { get; set; }
public RelayCommand CreateHardLinksCommand { get; set; }
public RelayCommand CheckUnusedCommand { get; set; }
public RelayCommand ImportLegacyCommand { get; set; }
public AsyncRelayCommand CheckFilesCommand { get; set; }
public AsyncRelayCommand CheckPiecesCommand { get; set; }
public RelayCommand FolderBrowseTorrentFileCommand { get; set; }
public RelayCommand FolderBrowseTorrentDLPathCommand { get; set; }
public AsyncRelayCommand LinkToTorrentCommand { get; set; }
public RelayCommand CopyFilesCommand { get; set; }
public MainWindowLogic(MainWindowControls controls, SynchronizationContext uicontext)
{
CheckDirs();
UIContext = uicontext;
Controls = controls;
LogList = new();
OpenAddInstanceCommand = new(OpenAddInstanceWindow);
DeleteInstanceCommand = new(DeleteInstance, CanDoActionWithInstance);
CreateHardLinksCommand = new(CreateHardLinks, CanDoActionWithInstance);
CheckUnusedCommand = new(CheckForUnusedFiles);
ImportLegacyCommand = new(ImportLegacyInstances);
CheckFilesCommand = new(CheckFiles, (ex) => LogList.Add(ex.Message));
CheckPiecesCommand = new(CheckPieces, (ex) => LogList.Add(ex.Message));
FolderBrowseTorrentFileCommand = new(FolderBrowseTorrentPath);
FolderBrowseTorrentDLPathCommand = new(FolderBrowserTorrentDLPath);
CopyFilesCommand = new(CopyFiles, CanDoActionWithInstance);
LinkToTorrentCommand = new(LinkToTorrent, (ex) => LogList.Add(ex.Message));
MatchedList = new();
RelativePath = string.Empty;
UpdateInstanceList();
IsFree = true;
}
public async Task<Instance> GetSelectedInstance()
{
using FileStream openStream = File.OpenRead(Path.Combine(instanceDir, SelectedInstance.InstanceName + ".json"));
return await JsonSerializer.DeserializeAsync<Instance>(openStream);
}
public void OpenAddInstanceWindow(object o)
{
AddInstanceWindow window = new();
window.ShowDialog();
UpdateInstanceList();
}
public void DeleteInstance(object o)
{
try
{
IsFree = false;
if (MessageBox.Show($"Delete {SelectedInstance.InstanceName}? This will not delete the actual files.",
"Delete instance", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
File.Delete(Path.Join(instanceDir, SelectedInstance.InstanceName + ".json"));
LogList.Add($"Instance {SelectedInstance.InstanceName} deleted");
}
else
{
LogList.Add("Delete operation aborted.");
}
UpdateInstanceList();
IsFree = true;
}
catch (Exception e)
{
LogList.Add(e.Message);
}
}
public async void CreateHardLinks(object o)
{
try
{
IsFree = false;
Progress = 0;
if (MessageBox.Show($"About to create hard links from {SelectedInstance.InstanceName}. " +
$"Proceed to link target directory selection?", "Create hard links", MessageBoxButtons.YesNo,
MessageBoxIcon.Question) == DialogResult.Yes)
{
FolderBrowserDialog folderBrowser = new FolderBrowserDialog();
if (folderBrowser.ShowDialog() == DialogResult.OK)
{
string selectedPath = folderBrowser.SelectedPath;
using FileStream openStream = File.OpenRead(Path.Combine(instanceDir, SelectedInstance.InstanceName + ".json"));
Instance inst = await JsonSerializer.DeserializeAsync<Instance>(openStream);
ProgressStep = 100D / inst.TotalFileCount;
foreach (string dir in inst.DirectoryList)
{
if (!Directory.Exists(Path.Combine(selectedPath, dir)))
{
Directory.CreateDirectory(Path.Combine(selectedPath, dir));
}
}
LogList.Add("Checking for duplicate files...");
int dupes = CheckForDupes(inst, selectedPath);
bool proceed = false;
if (dupes > 0)
{
if (MessageBox.Show($"{dupes} duplicate files already exist in the target directory. " +
$"Do you want to delete each one before linking new ones? " +
$"'No' Cancels the operation entirely", "Duplicate files detected",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
proceed = true;
foreach (var file in inst.FileList)
{
if (File.Exists(Path.Combine(selectedPath, file.RelativePath, file.FileName)))
{
File.Delete(Path.Combine(selectedPath, file.RelativePath, file.FileName));
//LogList.Add($"Deleting {Path.Combine(selectedPath, file.RelativePath, file.FileName)}");
}
}
}
else
{
LogList.Add("Link operation aborted.");
IsFree = true;
return;
}
}
if (dupes == 0 || proceed == true)
{
LogList.Add("Linking...");
foreach (var file in inst.FileList)
{
await Task.Run(() => CreateHardLink(Path.Combine(selectedPath, file.RelativePath, file.FileName),
Path.Combine(dbDir, file.HashedFileName), IntPtr.Zero));
Progress += ProgressStep;
}
LogList.Add("Done!");
IsFree = true;
}
}
else LogList.Add("Link operation aborted.");
}
else LogList.Add("Link operation aborted.");
IsFree = true;
}
catch (Exception e)
{
LogList.Add(e.Message);
}
}
public void CheckDirs()
{
if (Directory.Exists(dbDir) == false)
Directory.CreateDirectory(dbDir);
if (Directory.Exists(instanceDir) == false)
Directory.CreateDirectory(instanceDir);
}
public int CheckForDupes(Instance inst, string targetPath)
{
int exists = 0;
foreach (var file in inst.FileList)
{
if (File.Exists(Path.Combine(targetPath, file.RelativePath, file.FileName)))
{
exists++;
}
}
return exists;
}
public async void UpdateInstanceList()
{
IsFree = false;
InstanceList = new ObservableCollection<InstanceListEntry>();
foreach (var file in Directory.GetFiles(instanceDir, "*.json", SearchOption.TopDirectoryOnly))
{
using FileStream openStream = File.OpenRead(file);
Instance inst = await JsonSerializer.DeserializeAsync<Instance>(openStream);
InstanceListEntry entry = new(inst);
InstanceList.Add(entry);
openStream.Close();
}
LogList.Add("Instance list updated.");
UpdateDBSize();
IsFree = true;
}
public async void CheckForUnusedFiles(object o)
{
IsFree = false;
HashSet<string> existingFiles = new();
foreach (var file in Directory.GetFiles(dbDir, "*"))
{
existingFiles.Add(Path.GetFileName(file));
}
foreach (var instJson in Directory.GetFiles(instanceDir, "*.json", SearchOption.TopDirectoryOnly))
{
using FileStream openStream = File.OpenRead(instJson);
Instance inst = await JsonSerializer.DeserializeAsync<Instance>(openStream);
foreach (var file in inst.FileList)
{
existingFiles.Remove(file.HashedFileName);
}
}
if (existingFiles.Count > 0)
{
if (MessageBox.Show($"{existingFiles.Count} unused files found. Delete?", "Unused files found", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
foreach (var file in existingFiles)
{
File.Delete(Path.Combine(dbDir, file));
}
LogList.Add("Unused files deleted.");
UpdateInstanceList();
}
else
{
LogList.Add("Unused files deletion aborted.");
}
}
else
{
MessageBox.Show("No unused files found.", "No unused files", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
IsFree = true;
}
public bool CanDoActionWithInstance(object o)
{
if (SelectedInstance == null || !IsFree)
return false;
else return true;
}
public void ImportLegacyInstances(object o)
{
IsFree = false;
OpenFileDialog fileDialog = new();
fileDialog.Filter = "Legacy DBInfo|*.xml";
fileDialog.Title = "Select legacy DBInfo.xml";
if (fileDialog.ShowDialog() == DialogResult.OK)
{
XmlSerializer serializer = new(typeof(DBInfo));
FileStream fs = new(fileDialog.FileName, FileMode.Open);
XmlReader reader = XmlReader.Create(fs);
DBInfo info = (DBInfo)serializer.Deserialize(reader);
foreach (var instance in info.InstanceList)
{
if (File.Exists(Path.Combine(instanceDir, instance.InstanceName + ".json")))
{
LogList.Add($"Instance {instance.InstanceName} already exists. Not importing.");
}
else
{
List<InstanceFile> fileList = new();
HashSet<string> uniqueDirs = new();
foreach (var fileInfo in instance.InstanceFiles)
{
string relativePath = fileInfo.Location;
if (fileInfo.Location.StartsWith(@"\"))
{
relativePath = fileInfo.Location.Substring(1);
//LogList.Add(relativePath);
//break;
}
uniqueDirs.Add(relativePath);
InstanceFile newFile = new(fileInfo.OriginalFileName, relativePath, fileInfo.SizeBytes, fileInfo.HashedFileName);
fileList.Add(newFile);
}
List<string> dirs = new();
foreach (var dir in uniqueDirs)
{
dirs.Add(dir);
}
Instance newInstance = new(fileList, dirs, instance.Size, instance.Entries, instance.InstanceName);
var options = new JsonSerializerOptions { WriteIndented = true };
newInstance.SaveToFile(options);
LogList.Add($"Instance {instance.InstanceName} imported.");
}
}
UpdateInstanceList();
LogList.Add("Importing finished");
fs.Close();
}
else
{
LogList.Add("Import operation aborted.");
}
IsFree = true;
}
public async Task CheckFiles()
{
await Task.Run(() => CheckFilesJob());
}
public async void CheckFilesJob()
{
if (SelectedInstance == null)
{
MessageBox.Show("Please select an instance", "No instance selected", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
else if (string.IsNullOrEmpty(TorrentFilePath))
{
MessageBox.Show("Please select a torrent file", "No torrent file selected", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
else
{
Progress = 0;
UIContext.Send(x => MatchedList.Clear(), null);
var parser = new BencodeParser();
List<string> matchedList = new();
Torrent torrent = parser.Parse<Torrent>(TorrentFilePath);
ProgressStep = 100D / torrent.Files.Count;
using FileStream openStream = File.OpenRead(Path.Combine(instanceDir, SelectedInstance.InstanceName + ".json"));
Instance inst = await JsonSerializer.DeserializeAsync<Instance>(openStream);
IsFree = false;
foreach (var file in torrent.Files)
{
string? relPath = Path.GetDirectoryName(file.FullPath);
if (relPath == null)
{
relPath = string.Empty;
}
if (relPath.StartsWith(RelativePath))
{
string relPathQ = relPath;
if (relPath != string.Empty && !string.IsNullOrEmpty(RelativePath))
{
if (!Path.EndsInDirectorySeparator(RelativePath))
{
RelativePath += Path.DirectorySeparatorChar;
}
relPathQ = relPath.Substring(RelativePath.Length);
}
var instQuery = inst.FileList.Select(f => f).Where(f => f.RelativePath == relPathQ)
.Select(f => f).Where(f => f.FileName == file.FileName).Select(f => f)
.Where(f => f.FileSize == file.FileSize).Select(f => f);
if (instQuery.Count() > 0)
{
matchedList.Add(Path.Combine(instQuery.Single().RelativePath, instQuery.Single().FileName));
//UIContext.Send(x => MatchedList.Add(Path.Combine(instQuery.Single().RelativePath, instQuery.Single().FileName)), null);
}
}
Progress += ProgressStep;
}
UIContext.Send(x => MatchedList = matchedList, null);
UIContext.Send(x => LogList.Add($"Matched {matchedList.Count} out of {torrent.Files.Count} files (compared names and sizes)."), null);
//LogList.Add($"Matched {MatchedList.Count} out of {torrent.Files.Count} files (compared names and sizes).");
if (MatchedList.Count == 0)
{
UIContext.Send(x => LogList.Add(@"Check if your relative path is correct. (example: contents\data)"), null);
//LogList.Add("Check if your relative path is correct.");
}
else
{
DidCheckFiles = true;
}
IsFree = true;
}
}
public async Task CheckPieces()
{
DidCheckFiles = false;
await Task.Run(() => CheckPiecesJob());
}
public async void CheckPiecesJob()
{
Progress = 0;
IsFree = false;
var parser = new BencodeParser();
Torrent torrent = parser.Parse<Torrent>(TorrentFilePath);
ProgressStep = 100D / (torrent.Files.Count + 1);
List<string> torrentFiles = new();
UIContext.Send(x => LogList.Add($"Piece length: {torrent.PieceSize}"), null);
UIContext.Send(x => LogList.Add($"Number of pieces: {torrent.NumberOfPieces}"), null);
UIContext.Send(x => LogList.Add("Beginning piece check, this might take a while..."), null);
// make a list with individual pieces
List<byte[]> torrentPieceList = new();
for (int i = 0; i < torrent.Pieces.Length; i += 20)
{
byte[] piece = new byte[20];
Array.Copy(torrent.Pieces, i, piece, 0, 20);
torrentPieceList.Add(piece);
}
using FileStream openStream = File.OpenRead(Path.Combine(instanceDir, SelectedInstance.InstanceName + ".json"));
Instance inst = await JsonSerializer.DeserializeAsync<Instance>(openStream);
TorrentPiecer piecer = new(torrent.TotalSize, torrent.PieceSize, torrent.NumberOfPieces);
foreach (var file in torrent.Files)
{
string? relPath = Path.GetDirectoryName(file.FullPath);
if (relPath == null)
{
relPath = string.Empty;
}
var iQ = inst.FileList.Select(f => f).Where(f => Path.Combine(RelativePath, f.RelativePath) == relPath)
.Select(f => f).Where(f => f.FileName == file.FileName).Select(f => f)
.Where(f => f.FileSize == file.FileSize).Select(f => f);
if (iQ.Count() > 0)
{
byte[] fileBytes = File.ReadAllBytes(Path.Combine(dbDir, iQ.Single().HashedFileName));
piecer.PushFile(Path.Join(RelativePath, iQ.Single().RelativePath, iQ.Single().FileName),
iQ.Single().HashedFileName, fileBytes);
}
else
{
byte[] zeroBytes = new byte[file.FileSize];
Array.Fill<byte>(zeroBytes, 0);
piecer.PushFile(file.FullPath, "NO LOCAL FILE", zeroBytes);
}
Progress += ProgressStep;
}
// compare pieces
List<long> badPieces = new();
if (piecer.Pieces.Count == torrentPieceList.Count)
{
for (int i = 0; i < torrent.NumberOfPieces; i++)
{
if (!piecer.Pieces[i].SequenceEqual(torrentPieceList[i]))
{
badPieces.Add(i);
}
}
UIContext.Send(x => LogList.Add($"Piece check finished. {torrent.NumberOfPieces - badPieces.Count} out of {torrent.NumberOfPieces} pieces matched."), null);
BadPieces = badPieces;
FilePieceMap = piecer.FilePieceMap;
DidCheckPieces = true;
}
else
{
UIContext.Send(x => LogList.Add("Piece count does not match, something went terribly wrong."), null);
}
Progress += ProgressStep;
IsFree = true;
}
public async Task LinkToTorrent()
{
await Task.Run(() => LinkToTorrentFunc());
}
public void LinkToTorrentFunc()
{
if (BadPieces == null)
{
MessageBox.Show("Check pieces first.");
}
else if (FilePieceMap == null)
{
MessageBox.Show("Check pieces first");
}
else if (string.IsNullOrEmpty(TorrentDownloadPath))
{
MessageBox.Show("Please input the torrent download path", "Torrent download path empty",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
DidCheckPieces = false;
UIContext.Send(x => LogList.Add("Linking..."), null);
Progress = 0;
DidCheckFiles = false;
ProgressStep = 100D / FilePieceMap.Keys.Count;
foreach (var file in FilePieceMap)
{
string getDir = Path.GetDirectoryName(file.Key.FileName);
if (getDir != null)
{
if (!Directory.Exists(Path.Combine(TorrentDownloadPath, getDir)))
{
Directory.CreateDirectory(Path.Combine(TorrentDownloadPath, getDir));
}
}
if (!file.Value.Select(p => p).Intersect(BadPieces).Any())
{
if (!File.Exists(Path.Combine(TorrentDownloadPath, file.Key.FileName)))
{
CreateHardLink(Path.Combine(TorrentDownloadPath, file.Key.FileName),
Path.Combine(dbDir, file.Key.HashedFileName), IntPtr.Zero);
//LogList.Add($"Linking {Path.Combine(TorrentDownloadPath, file.Key.FileName)}");
}
}
Progress += ProgressStep;
}
UIContext.Send(x => LogList.Add("Linking finished"), null);
UIContext.Send(x => BadPieces = null, null);
UIContext.Send(x => FilePieceMap = null, null);
}
}
public void FolderBrowseTorrentPath(object o)
{
OpenFileDialog fileDialog = new();
fileDialog.Filter = "Torrent file|*.torrent";
fileDialog.Title = "Select a torrent file";
if (fileDialog.ShowDialog() == DialogResult.OK)
{
TorrentFilePath = fileDialog.FileName;
}
}
public void FolderBrowserTorrentDLPath(object o)
{
FolderBrowserDialog dialog = new();
dialog.ShowDialog();
TorrentDownloadPath = dialog.SelectedPath;
dialog.Dispose();
}
public bool CheckIfCanLinkToTorrent(object o)
{
if (!IsFree || BadPieces == null || FilePieceMap == null || string.IsNullOrEmpty(TorrentDownloadPath))
{
return false;
}
else return true;
}
public bool CheckIfCanCheckFiles(object o)
{
if (!IsFree || SelectedInstance == null || string.IsNullOrEmpty(TorrentFilePath))
{
return false;
}
else return true;
}
public void UpdateDBSize()
{
long dbSize = 0;
DirectoryInfo di = new(dbDir);
foreach (var file in di.GetFiles())
{
dbSize += file.Length;
}
DBSize = Instance.ReadableSize(dbSize);
long instTotal = 0;
foreach (var inst in InstanceList)
{
instTotal += inst.TotalFileSize;
}
Savings = Instance.ReadableSize(instTotal - dbSize);
var drives = DriveInfo.GetDrives();
foreach (var drive in drives)
{
if (currentDir.StartsWith(drive.Name))
{
FreeSpace = Instance.ReadableSize(drive.AvailableFreeSpace);
}
}
}
public async void CopyFiles(object o)
{
try
{
IsFree = false;
if (MessageBox.Show("This will copy all hashed files of an instance to your selected destination. " +
"Does not overwrite files. Proceed to destination selection?", "Copy files", MessageBoxButtons.YesNo,
MessageBoxIcon.Question) == DialogResult.Yes)
{
FolderBrowserDialog folderDialog = new();
if (folderDialog.ShowDialog() == DialogResult.OK)
{
string destDir = folderDialog.SelectedPath;
Progress = 0;
ProgressStep = 100D / SelectedInstance.FileCount;
Instance instance = await GetSelectedInstance();
foreach (var instanceFile in instance.FileList)
{
string destinationFile = Path.Combine(destDir, instanceFile.HashedFileName);
if (!File.Exists(destinationFile))
{
await Task.Run(() => File.Copy(Path.Combine(dbDir, instanceFile.HashedFileName), destinationFile));
}
else
{
LogList.Add(destinationFile + " already exists.");
}
Progress += ProgressStep;
}
}
folderDialog.Dispose();
}
Progress = 0;
IsFree = true;
}
catch (Exception e)
{
LogList.Add(e.Message);
}
}
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool CreateHardLink(
string lpFileName,
string lpExistingFileName,
IntPtr lpSecurityAttributes
);
}
}