Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9178e9ad72 | ||
|
|
ce9a8e11e0 | ||
|
|
080a727530 | ||
|
|
a410cb1d22 | ||
|
|
23b99cd520 | ||
|
|
4abe0efc11 | ||
|
|
3b36299b12 | ||
|
|
aff3cc2ec8 | ||
|
|
f32310d05e | ||
|
|
29f7ff3c3b | ||
|
|
da502b93b1 |
@@ -0,0 +1,78 @@
|
||||
<Window x:Class="LincleLINK.AddInstanceWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:LincleLINK"
|
||||
mc:Ignorable="d"
|
||||
Title="Add instance" Height="450" Width="800">
|
||||
<Grid Margin="5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="25"></RowDefinition>
|
||||
<RowDefinition></RowDefinition>
|
||||
<RowDefinition Height="25"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Instance name" VerticalAlignment="Center"></TextBlock>
|
||||
<TextBox Grid.Row="1" Text="{Binding InstanceName, Mode=TwoWay}" IsEnabled="{Binding UIEnabled}"></TextBox>
|
||||
<TextBlock Grid.Row="2" VerticalAlignment="Center" Text="Data path"></TextBlock>
|
||||
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*" MaxWidth="100" MinWidth="100"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox Grid.Column="0" Text="{Binding DataPath, Mode=TwoWay}" IsEnabled="{Binding UIEnabled}"></TextBox>
|
||||
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseCommand}" IsEnabled="{Binding UIEnabled}"></Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<RadioButton Grid.Column="0" Content="Copy files" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding IsCopyChecked, Mode=TwoWay}" IsEnabled="{Binding UIEnabled}"></RadioButton>
|
||||
<RadioButton Grid.Column="1" Content="Move files" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding IsMoveChecked, Mode=TwoWay}" IsEnabled="{Binding UIEnabled}"></RadioButton>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Button Grid.Row="5" Content="Create instance" Command="{Binding MakeInstanceCommand}" IsEnabled="{Binding UIEnabled}"></Button>
|
||||
|
||||
<ScrollViewer Grid.Row="6" x:Name="LogScroller">
|
||||
<VirtualizingStackPanel VirtualizingStackPanel.VirtualizationMode="Recycling">
|
||||
<ItemsControl ItemsSource="{Binding LogList}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</VirtualizingStackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<ProgressBar Grid.Row="7" Value="{Binding Progress}">
|
||||
<ProgressBar.Foreground>
|
||||
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
|
||||
<LinearGradientBrush.RelativeTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform CenterY="0.5" CenterX="0.5"/>
|
||||
<SkewTransform CenterX="0.5" CenterY="0.5" AngleX="0" AngleY="0"/>
|
||||
<RotateTransform Angle="90" CenterX="0.5" CenterY="0.5"/>
|
||||
<TranslateTransform/>
|
||||
</TransformGroup>
|
||||
</LinearGradientBrush.RelativeTransform>
|
||||
<GradientStop Color="#FF1C47F5"/>
|
||||
<GradientStop Color="#FF09F718" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</ProgressBar.Foreground>
|
||||
</ProgressBar>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AddInstanceWindow.xaml
|
||||
/// </summary>
|
||||
public partial class AddInstanceWindow : Window
|
||||
{
|
||||
public AddInstanceWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new AddInstanceWindowLogic();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="LincleLINK.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:LincleLINK"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>True</UseWindowsForms>
|
||||
<ApplicationIcon>LL_logo.ico</ApplicationIcon>
|
||||
<AssemblyVersion>2.1.0</AssemblyVersion>
|
||||
<FileVersion>2.1.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="LL_logo.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BencodeNET" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class AddInstanceWindowLogic : ViewModelBase
|
||||
{
|
||||
private string _instanceName;
|
||||
public string InstanceName
|
||||
{
|
||||
get { return _instanceName; }
|
||||
set
|
||||
{
|
||||
_instanceName = value;
|
||||
OnPropertyChanged(nameof(InstanceName));
|
||||
}
|
||||
}
|
||||
|
||||
private string _dataPath;
|
||||
public string DataPath
|
||||
{
|
||||
get { return _dataPath; }
|
||||
set
|
||||
{
|
||||
_dataPath = value;
|
||||
OnPropertyChanged(nameof(DataPath));
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isCopyChecked;
|
||||
public bool IsCopyChecked
|
||||
{
|
||||
get { return _isCopyChecked; }
|
||||
set
|
||||
{
|
||||
_isCopyChecked = value;
|
||||
OnPropertyChanged(nameof(IsCopyChecked));
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isMoveChecked;
|
||||
public bool IsMoveChecked
|
||||
{
|
||||
get { return _isMoveChecked; }
|
||||
set
|
||||
{
|
||||
_isMoveChecked = value;
|
||||
OnPropertyChanged(nameof(IsMoveChecked));
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableCollection<string> _logList;
|
||||
public ObservableCollection<string> LogList
|
||||
{
|
||||
get { return _logList; }
|
||||
set
|
||||
{
|
||||
_logList = value;
|
||||
}
|
||||
}
|
||||
|
||||
private double ProgressStep;
|
||||
private double _progress;
|
||||
public double Progress
|
||||
{
|
||||
get { return _progress; }
|
||||
set
|
||||
{
|
||||
_progress = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
|
||||
private bool _uIEnabled;
|
||||
public bool UIEnabled
|
||||
{
|
||||
get { return _uIEnabled; }
|
||||
set
|
||||
{
|
||||
_uIEnabled = value;
|
||||
OnPropertyChanged(nameof(UIEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand MakeInstanceCommand { get; set; }
|
||||
public RelayCommand BrowseCommand { get; set; }
|
||||
|
||||
public AddInstanceWindowLogic()
|
||||
{
|
||||
MakeInstanceCommand = new RelayCommand(MakeInstance);
|
||||
BrowseCommand = new RelayCommand(BrowseForFolder);
|
||||
IsCopyChecked = true;
|
||||
Progress = 0;
|
||||
LogList = new();
|
||||
UIEnabled = true;
|
||||
}
|
||||
|
||||
public void BrowseForFolder(object o)
|
||||
{
|
||||
FolderBrowserDialog dialog = new();
|
||||
dialog.ShowDialog();
|
||||
DataPath = dialog.SelectedPath;
|
||||
dialog.Dispose();
|
||||
}
|
||||
|
||||
public void MakeInstance(object o)
|
||||
{
|
||||
LogList.Add("Validating...");
|
||||
if (ValidateInstance())
|
||||
{
|
||||
LogList.Add("Good to go!");
|
||||
try
|
||||
{
|
||||
CreateInstance();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogList.Add(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ValidateInstance()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(InstanceName))
|
||||
{
|
||||
System.Windows.Forms.MessageBox.Show("Instance name cannot be empty.",
|
||||
"Empty instance name", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DataPath))
|
||||
{
|
||||
System.Windows.Forms.MessageBox.Show("Data path cannot be empty.",
|
||||
"Empty data path", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] existingInstances = Directory.GetFiles(MainWindowLogic.instanceDir, "*.json");
|
||||
foreach (string instance in existingInstances)
|
||||
{
|
||||
if (InstanceName.ToLower() == Path.GetFileNameWithoutExtension(instance).ToLower())
|
||||
{
|
||||
System.Windows.Forms.MessageBox.Show("Instance with this name already exists.",
|
||||
"Duplicate instance name", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (char c in InstanceName)
|
||||
{
|
||||
if (Path.GetInvalidFileNameChars().Contains(c))
|
||||
{
|
||||
System.Windows.Forms.MessageBox.Show("Instance name contains invalid characters.",
|
||||
"Invalid characters", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (char c in DataPath)
|
||||
{
|
||||
if (Path.GetInvalidPathChars().Contains(c))
|
||||
{
|
||||
System.Windows.Forms.MessageBox.Show("Data path contains invalid characters",
|
||||
"Invalid characters", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async void CreateInstance()
|
||||
{
|
||||
UIEnabled = false;
|
||||
Progress = 0;
|
||||
List<InstanceFile> iFiles = new();
|
||||
List<string> directories = new();
|
||||
long totalSize = 0;
|
||||
int alreadyExists = 0;
|
||||
long newsize = 0;
|
||||
|
||||
DirectoryInfo dInfo = new(DataPath);
|
||||
var allFiles = dInfo.GetFiles("*", SearchOption.AllDirectories);
|
||||
ProgressStep = 100D / (allFiles.Length + 2);
|
||||
LogList.Add("Hashing...");
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
string relativePath = Path.GetRelativePath(DataPath, Path.GetDirectoryName(file.FullName));
|
||||
if (relativePath == ".")
|
||||
{
|
||||
relativePath = string.Empty;
|
||||
}
|
||||
string hash = await Task.Run(() => GetMD5Checksum(file.FullName));
|
||||
string hashedFileName = hash + file.Extension;
|
||||
//string hashedFileName = "test";
|
||||
LogList.Add($"Hashing {file.FullName}");
|
||||
if (IsCopyChecked == true && IsMoveChecked == false)
|
||||
{
|
||||
if (!File.Exists(Path.Combine(MainWindowLogic.dbDir, hashedFileName)))
|
||||
{
|
||||
await Task.Run(() => File.Copy(file.FullName, Path.Combine(MainWindowLogic.dbDir, hashedFileName)));
|
||||
newsize += file.Length;
|
||||
}
|
||||
else alreadyExists++;
|
||||
}
|
||||
else if (IsCopyChecked == false && IsMoveChecked == true)
|
||||
{
|
||||
if (!File.Exists(Path.Combine(MainWindowLogic.dbDir, hashedFileName)))
|
||||
{
|
||||
await Task.Run(() => File.Move(file.FullName, Path.Combine(MainWindowLogic.dbDir, hashedFileName)));
|
||||
newsize += file.Length;
|
||||
}
|
||||
else alreadyExists++;
|
||||
}
|
||||
|
||||
InstanceFile iFile = new(file.Name, relativePath, file.Length, hashedFileName);
|
||||
iFiles.Add(iFile);
|
||||
totalSize += file.Length;
|
||||
Progress += ProgressStep;
|
||||
}
|
||||
LogList.Add("Collecting directories...");
|
||||
foreach (var dir in Directory.GetDirectories(DataPath, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
directories.Add(Path.GetRelativePath(DataPath, dir));
|
||||
}
|
||||
Progress += ProgressStep;
|
||||
|
||||
Instance instance = new(iFiles, directories, totalSize, allFiles.Length, InstanceName);
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
LogList.Add("Saving instance to file...");
|
||||
instance.SaveToFile(options);
|
||||
Progress += ProgressStep;
|
||||
LogList.Add($"Instance added. {alreadyExists} files already exist. {Instance.ReadableSize(newsize)} added to the db.");
|
||||
UIEnabled = true;
|
||||
}
|
||||
|
||||
public static string GetMD5Checksum(string fileName)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
using var stream = File.OpenRead(fileName);
|
||||
var hash = md5.ComputeHash(stream);
|
||||
return BitConverter.ToString(hash).Replace("-", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public abstract class AsyncCommandBase : ICommand
|
||||
{
|
||||
private readonly Action<Exception> _onException;
|
||||
|
||||
private bool _isExecuting;
|
||||
public bool IsExecuting
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isExecuting;
|
||||
}
|
||||
set
|
||||
{
|
||||
_isExecuting = value;
|
||||
CanExecuteChanged?.Invoke(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged;
|
||||
|
||||
public AsyncCommandBase(Action<Exception> onException)
|
||||
{
|
||||
_onException = onException;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return !IsExecuting;
|
||||
}
|
||||
|
||||
public async void Execute(object parameter)
|
||||
{
|
||||
IsExecuting = true;
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteAsync(parameter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onException?.Invoke(ex);
|
||||
}
|
||||
|
||||
IsExecuting = false;
|
||||
}
|
||||
|
||||
protected abstract Task ExecuteAsync(object parameter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class AsyncRelayCommand : AsyncCommandBase
|
||||
{
|
||||
private readonly Func<Task> _callback;
|
||||
|
||||
public AsyncRelayCommand(Func<Task> callback, Action<Exception> onException) : base(onException)
|
||||
{
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(object parameter)
|
||||
{
|
||||
await _callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class PropertyChangedBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
Application.Current.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
PropertyChangedEventHandler handler = PropertyChanged;
|
||||
if (handler != null)
|
||||
handler(this, new PropertyChangedEventArgs(propertyName));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
readonly Action<object> _execute;
|
||||
readonly Predicate<object> _canExecute;
|
||||
|
||||
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
|
||||
{
|
||||
if (execute == null)
|
||||
throw new NullReferenceException("execute");
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public RelayCommand(Action<object> execute) : this(execute, null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add { CommandManager.RequerySuggested += value; }
|
||||
remove { CommandManager.RequerySuggested -= value; }
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute == null ? true : _canExecute(parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
_execute.Invoke(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class Instance
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public long TotalFileSize { get; set; }
|
||||
public int TotalFileCount { get; set; }
|
||||
public string TotalFileSizeString { get; set; }
|
||||
public List<InstanceFile> FileList { get; set; }
|
||||
public List<string> DirectoryList { get; set; }
|
||||
|
||||
public Instance(List<InstanceFile> fileList, List<string> directoryList, long totalFileSize, int totalFileCount, string name)
|
||||
{
|
||||
Name = name;
|
||||
FileList = fileList;
|
||||
DirectoryList = directoryList;
|
||||
TotalFileSize = totalFileSize;
|
||||
TotalFileCount = totalFileCount;
|
||||
TotalFileSizeString = ReadableSize(totalFileSize);
|
||||
}
|
||||
|
||||
public static string ReadableSize(long size)
|
||||
{
|
||||
if (size < 1024f)
|
||||
{
|
||||
return $"{size} B";
|
||||
}
|
||||
else if (size > 1024f && size < 1048576f)
|
||||
{
|
||||
return $"{Math.Round(size / 1024f, 2)} KB";
|
||||
}
|
||||
else if (size > 1048576f && size < 1073741824f)
|
||||
{
|
||||
return $"{Math.Round(size / 1048576f, 2)} MB";
|
||||
}
|
||||
else if (size > 1073741824f)
|
||||
{
|
||||
return $"{Math.Round(size / 1073741824f, 2)} GB";
|
||||
}
|
||||
else return $"{Math.Round(size / 1099511627776f, 2)} TB";
|
||||
}
|
||||
|
||||
public void SaveToFile()
|
||||
{
|
||||
File.WriteAllText(Path.Combine(MainWindowLogic.instanceDir, Name + ".json"), JsonSerializer.Serialize(this));
|
||||
}
|
||||
|
||||
public void SaveToFile(JsonSerializerOptions options)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(MainWindowLogic.instanceDir, Name + ".json"), JsonSerializer.Serialize(this, options));
|
||||
}
|
||||
}
|
||||
|
||||
public class InstanceFile
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public string HashedFileName { get; set; }
|
||||
|
||||
public InstanceFile(string fileName, string relativePath, long fileSize, string hashedFileName)
|
||||
{
|
||||
FileName = fileName;
|
||||
RelativePath = relativePath;
|
||||
FileSize = fileSize;
|
||||
HashedFileName = hashedFileName;
|
||||
}
|
||||
}
|
||||
|
||||
public class InstanceListEntry
|
||||
{
|
||||
public string InstanceName { get; set; }
|
||||
public int FileCount { get; set; }
|
||||
public long TotalFileSize { get; set; }
|
||||
public string TotalFileSizeString { get; set; }
|
||||
|
||||
public InstanceListEntry(Instance inst)
|
||||
{
|
||||
InstanceName = inst.Name;
|
||||
FileCount = inst.FileList.Count;
|
||||
TotalFileSize = inst.TotalFileSize;
|
||||
TotalFileSizeString = inst.TotalFileSizeString;
|
||||
}
|
||||
}
|
||||
|
||||
public struct PassedFileInfo
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string HashedFileName { get; set; }
|
||||
|
||||
public PassedFileInfo(string fileName, string hashedFileName)
|
||||
{
|
||||
FileName = fileName;
|
||||
HashedFileName = hashedFileName;
|
||||
}
|
||||
}
|
||||
|
||||
public class TorrentPiecer
|
||||
{
|
||||
// main list of pieces
|
||||
public List<byte[]> Pieces { get; set; }
|
||||
|
||||
// properties for file-to-piece mapping
|
||||
public Dictionary<PassedFileInfo, HashSet<long>> FilePieceMap { get; set; }
|
||||
public HashSet<long> CurrentFilePieces { get; set; }
|
||||
|
||||
// imported values through the constructor
|
||||
private long TotalFileSize { get; set; }
|
||||
private long PieceSize { get; set; }
|
||||
private long NumberOfPieces { get; set; }
|
||||
|
||||
// working properties
|
||||
private byte[]? CurrentPiece { get; set; } // current piece being worked on
|
||||
private long CurrentPieceBytesLeft { get; set; } // free bytes remaining in the current piece
|
||||
private long CurrentPieceIndex { get; set; } // starts at 0
|
||||
private long CurrentFileBytesLeft { get; set; } // number of remaining bytes in the current file to work through
|
||||
private string CurrentFileName { get; set; } // name of the current file
|
||||
private long LastPieceSize { get; set; } // size of the last piece, calculated in the constructor
|
||||
|
||||
public TorrentPiecer(long totalFileSize, long pieceSize, long numberOfPieces)
|
||||
{
|
||||
TotalFileSize = totalFileSize;
|
||||
PieceSize = pieceSize;
|
||||
NumberOfPieces = numberOfPieces;
|
||||
|
||||
Pieces = new();
|
||||
FilePieceMap = new();
|
||||
|
||||
CurrentPieceIndex = 0;
|
||||
|
||||
// calculate the last piece size
|
||||
LastPieceSize = TotalFileSize - ((NumberOfPieces - 1) * PieceSize);
|
||||
}
|
||||
|
||||
public void PushFile(string fileName, string hashedFileName, byte[] fileBytes)
|
||||
{
|
||||
CurrentFileBytesLeft = fileBytes.Length;
|
||||
CurrentFileName = fileName;
|
||||
CurrentFilePieces = new();
|
||||
WorkBytes(fileBytes);
|
||||
PassedFileInfo pInfo = new(fileName, hashedFileName);
|
||||
FilePieceMap.Add(pInfo, CurrentFilePieces);
|
||||
}
|
||||
|
||||
private void WorkBytes(byte[] fileBytes)
|
||||
{
|
||||
// work through the file
|
||||
while (CurrentFileBytesLeft > 0)
|
||||
{
|
||||
if (CurrentPiece == null)
|
||||
{
|
||||
CreatePiece();
|
||||
}
|
||||
|
||||
CurrentFilePieces.Add(CurrentPieceIndex);
|
||||
|
||||
if (CurrentPieceBytesLeft > CurrentFileBytesLeft)
|
||||
{
|
||||
Array.Copy(fileBytes, fileBytes.Length - CurrentFileBytesLeft, CurrentPiece,
|
||||
CurrentPiece.Length - CurrentPieceBytesLeft, CurrentFileBytesLeft);
|
||||
CurrentPieceBytesLeft -= CurrentFileBytesLeft;
|
||||
CurrentFileBytesLeft = 0;
|
||||
}
|
||||
else if (CurrentPieceBytesLeft < CurrentFileBytesLeft)
|
||||
{
|
||||
Array.Copy(fileBytes, fileBytes.Length - CurrentFileBytesLeft, CurrentPiece,
|
||||
CurrentPiece.Length - CurrentPieceBytesLeft, CurrentPieceBytesLeft);
|
||||
CurrentFileBytesLeft -= CurrentPieceBytesLeft;
|
||||
HashPiece();
|
||||
}
|
||||
else if (CurrentPieceBytesLeft == CurrentFileBytesLeft)
|
||||
{
|
||||
Array.Copy(fileBytes, fileBytes.Length - CurrentFileBytesLeft, CurrentPiece,
|
||||
CurrentPiece.Length - CurrentPieceBytesLeft, CurrentPieceBytesLeft);
|
||||
CurrentFileBytesLeft = 0;
|
||||
HashPiece();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePiece()
|
||||
{
|
||||
if (CurrentPieceIndex == NumberOfPieces - 1)
|
||||
{
|
||||
CurrentPiece = new byte[LastPieceSize];
|
||||
CurrentPieceBytesLeft = LastPieceSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentPiece = new byte[PieceSize];
|
||||
CurrentPieceBytesLeft = PieceSize;
|
||||
}
|
||||
}
|
||||
|
||||
private void HashPiece()
|
||||
{
|
||||
SHA1 sha = SHA1.Create();
|
||||
byte[] hash = sha.ComputeHash(CurrentPiece);
|
||||
Pieces.Add(hash);
|
||||
CurrentPiece = null;
|
||||
CurrentPieceIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LincleLINK.Logic
|
||||
{
|
||||
public class Global
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class DBInfo
|
||||
{
|
||||
public List<DataInstance> InstanceList { get; set; }
|
||||
public int InstanceCount => InstanceList.Count;
|
||||
public long DBSize
|
||||
{
|
||||
get
|
||||
{
|
||||
long size = 0;
|
||||
DirectoryInfo dir = new("");
|
||||
foreach (var file in dir.GetFiles())
|
||||
{
|
||||
size += file.Length;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
public long IndividualSize
|
||||
{
|
||||
get
|
||||
{
|
||||
long size = 0;
|
||||
foreach (DataInstance instance in InstanceList)
|
||||
{
|
||||
size += instance.Size;
|
||||
}
|
||||
return size;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public DBInfo()
|
||||
{
|
||||
if (InstanceList == null)
|
||||
InstanceList = new();
|
||||
}
|
||||
|
||||
public void SaveToXml()
|
||||
{
|
||||
XmlSerializer serializer = new(typeof(DBInfo));
|
||||
//TextWriter writer = new StreamWriter(DBInfoPath);
|
||||
//serializer.Serialize(writer, this);
|
||||
}
|
||||
public void UpdateDBSize()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class DataInstance
|
||||
{
|
||||
public string InstanceName { get; set; }
|
||||
public List<InstanceFileInfo> InstanceFiles { get; set; }
|
||||
public int Entries { get; set; }
|
||||
public long Size { get; set; }
|
||||
public string PrettySize;
|
||||
|
||||
public DataInstance(string name, List<InstanceFileInfo> fileInfos, int entries, long size)
|
||||
{
|
||||
InstanceName = name;
|
||||
InstanceFiles = fileInfos;
|
||||
Entries = entries;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public DataInstance()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class InstanceFileInfo
|
||||
{
|
||||
public string OriginalFileName { get; set; }
|
||||
public string HashedFileName { get; set; }
|
||||
public string Location { get; set; }
|
||||
public long SizeBytes { get; set; }
|
||||
public string PrettySize;
|
||||
|
||||
public InstanceFileInfo(string orgFName, string hashFName, string loc, long size)
|
||||
{
|
||||
OriginalFileName = orgFName;
|
||||
HashedFileName = hashFName;
|
||||
Location = loc;
|
||||
SizeBytes = size;
|
||||
}
|
||||
public InstanceFileInfo()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
public class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged(string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<Window x:Class="LincleLINK.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:LincleLINK"
|
||||
mc:Ignorable="d"
|
||||
Title="LincleLINK "It never ends"" Height="450" Width="800">
|
||||
|
||||
<Grid x:Name="LayoutRoot" Margin="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="4*" MinHeight="100"></RowDefinition>
|
||||
<RowDefinition Height="5"></RowDefinition>
|
||||
<RowDefinition Height="1*" MinHeight="50"></RowDefinition>
|
||||
<RowDefinition Height="25" MinHeight="25"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<GridSplitter Grid.Row="1" Grid.ColumnSpan="4" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="#FF04DDF9"/>
|
||||
|
||||
<TabControl>
|
||||
<TabItem Header="Instances" TabIndex="0">
|
||||
<Grid x:Name="InstancesGrid">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="25"></RowDefinition>
|
||||
<RowDefinition Height="1*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" x:Name="InstancesGridButtons">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Button Grid.Column="0" Content="Add instance" Command="{Binding OpenAddInstanceCommand}" IsEnabled="{Binding IsFree}"/>
|
||||
<Button Grid.Column="1" Content="Link files" Command="{Binding CreateHardLinksCommand}"/>
|
||||
<Button Grid.Column="2" Content="Copy hashed files" Command="{Binding CopyFilesCommand}"/>
|
||||
<Button Grid.Column="3" Content="Check for unused files" Command="{Binding CheckUnusedCommand}" IsEnabled="{Binding IsFree}"></Button>
|
||||
<Button Grid.Column="4" Content="Delete instance" Command="{Binding DeleteInstanceCommand}"></Button>
|
||||
|
||||
</Grid>
|
||||
|
||||
<DataGrid Grid.Row="1" ItemsSource="{Binding InstanceList}" AutoGenerateColumns="False" CanUserReorderColumns="False"
|
||||
CanUserSortColumns="True" CanUserDeleteRows="False" CanUserAddRows="False" SelectionMode="Single"
|
||||
CanUserResizeRows="False" HeadersVisibility="Column" GridLinesVisibility="None"
|
||||
SelectionUnit="FullRow" IsReadOnly="True" SelectedItem="{Binding SelectedInstance, Mode=TwoWay}">
|
||||
<DataGrid.Resources>
|
||||
<Style TargetType="{x:Type DataGridCell}">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name" Width="3*" Binding="{Binding InstanceName}"></DataGridTextColumn>
|
||||
<DataGridTextColumn Header="File count" Width="1*" Binding="{Binding FileCount}"></DataGridTextColumn>
|
||||
<DataGridTextColumn Header="Instance size" Width="1*" Binding="{Binding TotalFileSizeString}" SortMemberPath="TotalFileSize"></DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
|
||||
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Link to torrent" TabIndex="1">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Instance to link from" VerticalAlignment="Center"></TextBlock>
|
||||
<ComboBox Grid.Row="1" VerticalAlignment="Center" IsEnabled="{Binding IsFree}" ItemsSource="{Binding InstanceList}" DisplayMemberPath="InstanceName" SelectedValue="{Binding SelectedInstance}" IsReadOnly="True"></ComboBox>
|
||||
<TextBlock Grid.Row="2" Text="Torrent file path" VerticalAlignment="Center"></TextBlock>
|
||||
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
<ColumnDefinition MaxWidth="100" MinWidth="100"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox Grid.Column="0" Text="{Binding TorrentFilePath}" IsEnabled="{Binding IsFree}"></TextBox>
|
||||
<Button Grid.Column="1" MaxWidth="100" IsEnabled="{Binding IsFree}" MinWidth="100" Content="Browse..." Command="{Binding FolderBrowseTorrentFileCommand}"></Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Row="4" Text="Torrent relative data path" VerticalAlignment="Center"></TextBlock>
|
||||
<TextBox Grid.Row="5" IsEnabled="{Binding IsFree}" Text="{Binding RelativePath, Mode=TwoWay}"></TextBox>
|
||||
<TextBlock Grid.Row="6" Text="Torrent download & link target location" VerticalAlignment="Center"></TextBlock>
|
||||
<Grid Grid.Row="7">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
<ColumnDefinition MaxWidth="100" MinWidth="100"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox Grid.Column="0" IsEnabled="{Binding IsFree}" Text="{Binding TorrentDownloadPath}"></TextBox>
|
||||
<Button Grid.Column="1" IsEnabled="{Binding IsFree}" MaxWidth="100" MinWidth="100" Content="Browse..." Command="{Binding FolderBrowseTorrentDLPathCommand}"></Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0" Content="Check files" IsEnabled="{Binding IsFree}" Command="{Binding CheckFilesCommand}"></Button>
|
||||
<Button Grid.Column="1" Content="Check pieces" IsEnabled="{Binding DidCheckFiles}" Command="{Binding CheckPiecesCommand}"></Button>
|
||||
<Button Grid.Column="2" Content="Start linking" IsEnabled="{Binding DidCheckPieces}" Command="{Binding LinkToTorrentCommand}" ></Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Row="9" Text="Matched instance files" VerticalAlignment="Center"></TextBlock>
|
||||
|
||||
<ScrollViewer Grid.Row="10" x:Name="MatchedFilesScroller">
|
||||
|
||||
<VirtualizingStackPanel VirtualizingStackPanel.VirtualizationMode="Recycling">
|
||||
<ItemsControl ItemsSource="{Binding MatchedList}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</VirtualizingStackPanel>
|
||||
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Other" TabIndex="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
<RowDefinition Height="20"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="{Binding DBSize, StringFormat=db folder size: {0}}"></TextBlock>
|
||||
<TextBlock Grid.Row="1" Text="{Binding Savings, StringFormat=You are saving: {0}}"></TextBlock>
|
||||
|
||||
<Button Grid.Row="3" Content="Import DBInfo.xml" IsEnabled="{Binding IsFree}" Command="{Binding ImportLegacyCommand}"></Button>
|
||||
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
|
||||
<ScrollViewer Grid.Row="2" x:Name="LogScroller">
|
||||
<VirtualizingStackPanel VirtualizingStackPanel.VirtualizationMode="Recycling">
|
||||
<ItemsControl ItemsSource="{Binding LogList}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</VirtualizingStackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<ProgressBar Grid.Row="3" Value="{Binding Progress}"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,45 @@
|
||||
using LincleLINK.Logic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace LincleLINK
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
MainWindowControls controls = new(LogScroller, MatchedFilesScroller);
|
||||
var uiContext = SynchronizationContext.Current;
|
||||
DataContext = new MainWindowLogic(controls, uiContext);
|
||||
}
|
||||
}
|
||||
|
||||
public struct MainWindowControls
|
||||
{
|
||||
public ScrollViewer LogScrollViewer;
|
||||
public ScrollViewer MatchedFilesScroller;
|
||||
|
||||
public MainWindowControls(ScrollViewer scroller, ScrollViewer matchedScroller)
|
||||
{
|
||||
LogScrollViewer = scroller;
|
||||
MatchedFilesScroller = matchedScroller;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
# WPFLinkTool
|
||||
WPF MVVM version of my link tool, faster, better, stronger.
|
||||
# LincleLINK
|
||||
|
||||
If you used my old versions, your xml instances won't load but your DB folder will be compatible.
|
||||
Just use the old tools to make links somewhere and add them as instances (copy files) using the WPF version.
|
||||
This program is designed to store all your arcade data in its own `db` folder. When you add an instance, it produces an MD5 hash of the file and stores it in the `db` folder. Only new and unique files are added, but the record of the file and its location in the original folder is also stored in the instance `.json` file.
|
||||
|
||||

|
||||
You can then create hard links to your desired destination(s).
|
||||
|
||||
***It is highly recommended to only add the `data` folder of a game as an instance.***
|
||||
***Also remember that hard links only work on the same partition, additionally editing/modifying hard-linked files will change the originals in the `db` so take extra care. Delete a hard-linked file first if you want to replace/modify it!***
|
||||
|
||||

|
||||
|
||||
+9
-3
@@ -1,9 +1,11 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31702.278
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32421.90
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WPFLinkTool", "WPFLinkTool\WPFLinkTool.csproj", "{F7A5BA83-57A5-40C8-87AB-14DC84AE7529}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WPFLinkTool", "WPFLinkTool\WPFLinkTool.csproj", "{F7A5BA83-57A5-40C8-87AB-14DC84AE7529}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LincleLINK", "LincleLINK\LincleLINK.csproj", "{12228E5C-A2E1-4648-90A0-C50CE051E613}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -15,6 +17,10 @@ Global
|
||||
{F7A5BA83-57A5-40C8-87AB-14DC84AE7529}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F7A5BA83-57A5-40C8-87AB-14DC84AE7529}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7A5BA83-57A5-40C8-87AB-14DC84AE7529}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{12228E5C-A2E1-4648-90A0-C50CE051E613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{12228E5C-A2E1-4648-90A0-C50CE051E613}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{12228E5C-A2E1-4648-90A0-C50CE051E613}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{12228E5C-A2E1-4648-90A0-C50CE051E613}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace WPFLinkTool
|
||||
foreach (var file in dBFiles)
|
||||
{
|
||||
await Task.Run(() => File.Delete(dbDir + @"\" + file));
|
||||
vm.Loggerr.Log($"Deleting {file}");
|
||||
//vm.Loggerr.Log($"Deleting {file}");
|
||||
vm.Progress += progressStep;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +43,14 @@ namespace WPFLinkTool
|
||||
{
|
||||
if (File.Exists(target + file.Location + @"\" + file.OriginalFileName))
|
||||
{
|
||||
vm.Loggerr.Log(@$"Deleting {target + file.Location + @"\" + file.OriginalFileName}");
|
||||
//vm.Loggerr.Log(@$"Deleting {target + file.Location + @"\" + file.OriginalFileName}");
|
||||
File.Delete(target + file.Location + @"\" + file.OriginalFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.Loggerr.Log("Linking...");
|
||||
|
||||
if (proceed)
|
||||
{
|
||||
var uniqueFolders = (from folders in vm.SelectedInstance.InstanceFiles
|
||||
@@ -61,7 +63,7 @@ namespace WPFLinkTool
|
||||
{
|
||||
if (!Directory.Exists(target + folder))
|
||||
{
|
||||
vm.Loggerr.Log($@"Creating directory {target + folder}");
|
||||
//vm.Loggerr.Log($@"Creating directory {target + folder}");
|
||||
await Task.Run(() => Directory.CreateDirectory(target + folder));
|
||||
}
|
||||
vm.Progress += progressStep;
|
||||
@@ -71,7 +73,7 @@ namespace WPFLinkTool
|
||||
{
|
||||
if (!File.Exists(target + file.Location + @"\" + file.OriginalFileName))
|
||||
{
|
||||
vm.Loggerr.Log($@"Linking {target + file.Location + @"\" + file.OriginalFileName}");
|
||||
//vm.Loggerr.Log($@"Linking {target + file.Location + @"\" + file.OriginalFileName}");
|
||||
await Task.Run(() => CreateHardLink(target + file.Location + @"\" + file.OriginalFileName,
|
||||
dbDir + @"\" + file.HashedFileName, IntPtr.Zero));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user