21 Commits
Author SHA1 Message Date
Radio fe02874536 Update README.md 2022-08-07 07:33:23 +02:00
Radio f0e60b418f Merge branch 'master' of https://github.com/Radioo/WPFLinkTool 2022-07-24 09:13:04 +02:00
Radio 7b79485ccc Add a warning for low disk space and display free space in the "Other" tab 2022-07-24 09:12:44 +02:00
Radio 5ce853bc5f Update README.md
Hopefully a clearer explanation
2022-07-24 08:08:00 +02:00
Radio 9178e9ad72 version increment 2022-06-18 11:26:42 +02:00
Radio ce9a8e11e0 Merge branch 'master' of https://github.com/Radioo/WPFLinkTool 2022-06-18 11:25:58 +02:00
Radio 080a727530 Add copy function 2022-06-18 11:25:50 +02:00
Radio a410cb1d22 Update README.md 2022-05-12 11:54:08 +02:00
Radio 23b99cd520 Fixes for linking 2022-05-12 11:36:33 +02:00
Radio 4abe0efc11 Prep for 2.0.0 2022-05-12 10:33:49 +02:00
Radio 3b36299b12 Add LincleLINK files 2022-05-12 08:21:05 +02:00
Radio aff3cc2ec8 Update solution file 2022-05-12 08:17:16 +02:00
Radio f32310d05e v1.2.1 2022-04-28 17:06:28 +02:00
Radio 29f7ff3c3b Update README.md 2022-03-24 19:27:58 +01:00
Radio da502b93b1 Update README.md 2022-03-24 19:20:16 +01:00
Radio 94966979c3 v1.2 - Logging 2022-03-24 19:15:16 +01:00
Radio eed41bbcc7 Merge branch 'master' of https://github.com/Radioo/WPFLinkTool 2022-03-15 06:51:59 +01:00
Radio 238cb9e984 v1.1.1 - Fix for making hard links 2022-03-15 06:51:50 +01:00
Radio 04c6ac3b8a Update README.md 2022-03-14 18:36:44 +01:00
Radio e7aa0b56c4 v1.1 - ACDATA CORE 2022-03-14 18:35:53 +01:00
Radio 80071ade8e Create FUNDING.yml 2022-02-03 11:30:04 +01:00
32 changed files with 2266 additions and 101 deletions
+13
View File
@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: radi0
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+78
View File
@@ -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>
+30
View File
@@ -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();
}
}
}
+9
View File
@@ -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>
+17
View File
@@ -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
{
}
}
+10
View File
@@ -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

+22
View File
@@ -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.2.0</AssemblyVersion>
<FileVersion>2.2.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="LL_logo.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BencodeNET" Version="4.0.0" />
</ItemGroup>
</Project>
+292
View File
@@ -0,0 +1,292 @@
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 async void MakeInstance(object o)
{
LogList.Add("Validating...");
UIEnabled = false;
bool validate_result = await Task.Run(() => ValidateInstance());
UIEnabled = true;
if (validate_result)
{
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;
}
}
// check if there is enough space for copying
if (IsCopyChecked)
{
DirectoryInfo files = new(DataPath);
long size_to_copy = 0;
foreach (var file in files.GetFiles("*", SearchOption.AllDirectories))
{
size_to_copy += file.Length;
}
var drives = DriveInfo.GetDrives();
long free_space = 0;
foreach (var drive in drives)
{
if (MainWindowLogic.currentDir.StartsWith(drive.Name))
{
free_space = drive.AvailableFreeSpace;
}
}
if (size_to_copy + 100000000 > free_space) // add a 100MB wiggle room
{
if (MessageBox.Show($"Current drive is low on disk space, do you want to continue? " +
$"Free space: {Instance.ReadableSize(free_space)}, Size of files about to copy: {Instance.ReadableSize(size_to_copy)}", "Low disk space!",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
{
return true;
}
else
{
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("-", "");
}
}
}
+58
View File
@@ -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));
});
}
}
}
+44
View File
@@ -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);
}
}
}
+211
View File
@@ -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++;
}
}
}
+13
View File
@@ -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
{
}
}
+103
View File
@@ -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()
{
}
}
}
+829
View File
@@ -0,0 +1,829 @@
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
);
}
}
+18
View File
@@ -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));
}
}
}
+190
View File
@@ -0,0 +1,190 @@
<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 &quot;It never ends&quot;" 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 &amp; 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>
<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>
<TextBlock Grid.Row="2" Text="{Binding FreeSpace, StringFormat=Free drive space {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>
+45
View File
@@ -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;
}
}
}
+35 -5
View File
@@ -1,7 +1,37 @@
# 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.
# How does it work?
![screenshot](https://stn.s-ul.eu/eMYGoMdP.png)
## File storage
The tool's main purpose is to store all your data in its own `db` folder. The files are renamed to their MD5 hash which ensures only unique files are added to the `db` folder.
## Instances
Instances are simply a record of: file names, their hashes and their location whithin the original data structure. When adding an instance, you have an option of choosing to copy or move files to the `db`. Moving is advised to reduce space and disk stress. ***It is highly recommended to only add the `data` folder of a game as an instance.***
Here's an example of one file record:
```
{
"FileName": "25063_pre.2dx",
"RelativePath": "sound\\25063",
"FileSize": 463806,
"HashedFileName": "7AFE6AC1B80128D44BA5357D4349B21A.2dx"
},
```
## Linking
When you've added at least one instance, you can link it to your desired destination. The tool will make hard links and keep the original file names and directory structure of the original files. ***Hard links only work on the same partition. If you wish to edit/replace a hard-linked file, delete it first! Directly modifying hard-linked files will alter the originals in the `db`!***
## Link to torrent
This feature was designed to allow you to prepare files for downloading a new style/version of a game. It uses your selected instance as a "base" and checks files against the pieces found in the `.torrent` file (https://en.wikipedia.org/wiki/Torrent_file). When the check is finished, you can link matched files to your desired destination and point your torrent client to check and download missing files. Using this feature ensures only files present in pieces with exact 100% match will be linked, this means your client should not overwrite your original files.
`Instance to link from` - The instance you want to use as a "base", use the one that will have the most common files with the torrent.
`Torrent file path` - The torrent file you wish to download.
`Torrent relative data path` - Location of the `data` folder within the torrent file structure. Usually it will be `contents\data\`. Using `Check files` will perform a quick file name and size comarsion and will let you know if you got the right path.
`Torrent download and link target location` - Your desired download location. The tool will recreate the folder structure found in the `.torrent` file. This is the location where you want to point your torrent client to download.
![Link to torrent usage](https://stn.s-ul.eu/mIFRwafZ.png)
# How does it save space?
When adding subsequent instances, only unique files will be added to the `db`. For example say you have IIDX28 added as an instance, then you add your IIDX27 data as well. Only files unique to IIDX27 will be copied/moved, in turn `db` size will increase by about 4GB instead of 60GB.
![screenshot](https://stn.s-ul.eu/O84VELWa.png)
+9 -3
View File
@@ -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
+1 -1
View File
@@ -10,7 +10,7 @@ namespace WPFLinkTool
public class Global
{
//public static string curDir = @"F:\Torrents\bemaniso.ws\HardLinkUI";
//public static string curDir = @"C:\Tools and stuff\WPFLinkTool";
//public static string curDir = @"C:\Tools&Stuff\LinkToolTest";
public static string curDir = Directory.GetCurrentDirectory();
public static string dbDir = curDir + @"\DB";
public static string xmlDir = curDir + @"\XML";
+28
View File
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace WPFLinkTool
{
public class Logger
{
public ObservableCollection<string> LogEntries;
public ScrollViewer Scroller;
public Logger(ScrollViewer scroller)
{
LogEntries = new();
Scroller = scroller;
}
public void Log(string message)
{
LogEntries.Add(DateTime.Now.ToString() + " " + message);
Scroller.ScrollToBottom();
}
}
}
+34 -30
View File
@@ -5,9 +5,20 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFLinkTool"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="500">
<Border Padding="10">
<StackPanel>
Title="kors k's How to save a lot of disk space for your totally legal japanese rhythm games" Height="400" Width="650">
<Border Padding="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="16" MinHeight="16"></RowDefinition>
<RowDefinition MaxHeight="24" MinHeight="24"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition MaxHeight="24" MinHeight="24"></RowDefinition>
<RowDefinition Height="0.3*"></RowDefinition>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
@@ -18,47 +29,40 @@
<TextBlock Grid.Column="1" Text="{Binding SumSize, StringFormat=Instance size sum: {0}}"></TextBlock>
</Grid>
<Grid Margin="0 2 0 2">
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Command="{Binding OpenAddInstanceWindow}" Grid.Column="0">Add instance</Button>
<Button IsEnabled="{Binding UIEnabled}" Command="{Binding OpenAddInstanceWindow}" Grid.Column="0">Add instance</Button>
<Button IsEnabled="{Binding LinkButtonEnabled}" Command="{Binding MakeHardLinksCommand}" Grid.Column="1">Make hard links</Button>
<Button IsEnabled="{Binding UIEnabled}" Command="{Binding DeleteUnusedCommand}" Grid.Column="2">Delete unused files</Button>
<Button IsEnabled="{Binding LinkButtonEnabled}" Command="{Binding DeleteInstanceCommand}" Grid.Column="3">Delete instance</Button>
</Grid>
<DataGrid Grid.Row="2" ItemsSource="{Binding InstanceList}" AutoGenerateColumns="False" SelectedItem="{Binding SelectedInstance, Mode=TwoWay}"
IsReadOnly="True" SelectionMode="Single" SelectionUnit="FullRow" CanUserReorderColumns="False"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="False" HeadersVisibility="Column"
ScrollViewer.CanContentScroll="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding InstanceName}" Width="3*"></DataGridTextColumn>
<DataGridTextColumn Header="File count" Binding="{Binding Entries}" Width="1*"></DataGridTextColumn>
<DataGridTextColumn Header="Instance size" Binding="{Binding PrettySize}" Width="1*" SortMemberPath="Size"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<ProgressBar Grid.Row="3" Value="{Binding Progress}" MinHeight="20"></ProgressBar>
<DockPanel MaxHeight="300" Margin="0 2 0 2">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<ItemsControl ItemsSource="{Binding InstanceList}">
<ItemsControl.ItemTemplate>
<ScrollViewer x:Name="Scroller" Grid.Row="4">
<ItemsControl Grid.Row="1" ItemsSource="{Binding LogEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<RadioButton x:Name="RdioBtn" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=DataContext.SelectedInstanceCommand}" CommandParameter="{Binding ElementName=InstName, Path=Text}" Grid.Column="0" GroupName="InstanceGroup"></RadioButton>
<TextBlock HorizontalAlignment="Center" x:Name="InstName" Grid.Column="1" Text="{Binding InstanceName}"></TextBlock>
<TextBlock HorizontalAlignment="Center" Grid.Column="2" Text="{Binding Entries, StringFormat={}{0} files}"></TextBlock>
<TextBlock HorizontalAlignment="Center" Grid.Column="3" Text="{Binding PrettySize}"></TextBlock>
</Grid>
<TextBox Text="{Binding Mode=OneWay}" TextWrapping="Wrap" Background="Transparent" BorderThickness="0" IsReadOnly="True"></TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<ProgressBar Margin="0 2 0 2" Value="{Binding Progress}" MinHeight="20"></ProgressBar>
<TextBlock Text="{Binding HeaderText}"></TextBlock>
</StackPanel>
</Grid>
</Border>
</Window>
+1 -1
View File
@@ -26,7 +26,7 @@ namespace WPFLinkTool
{
InitializeComponent();
SharedViewModel shared = new();
DataContext = new MainWindowViewModel(shared);
DataContext = new MainWindowViewModel(shared, Scroller);
}
}
}
@@ -22,6 +22,7 @@ namespace WPFLinkTool
{
IsExecuting = true;
vm.UIEnabled = false;
vm.LinkButtonEnabled = false;
HashSet<string> dBFiles = new();
DirectoryInfo dir = new(dbDir);
@@ -47,6 +48,7 @@ namespace WPFLinkTool
foreach (var file in dBFiles)
{
await Task.Run(() => File.Delete(dbDir + @"\" + file));
//vm.Loggerr.Log($"Deleting {file}");
vm.Progress += progressStep;
}
}
@@ -55,7 +57,9 @@ namespace WPFLinkTool
vm.Progress = 0;
vm.UIEnabled = true;
vm.LinkButtonEnabled = true;
IsExecuting = false;
vm.Loggerr.Log("Delete operation finished.");
}
}
}
@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static WPFLinkTool.Global;
@@ -14,66 +11,112 @@ namespace WPFLinkTool
public class MakeHardLinksCommand : AsyncCommandBase
{
private readonly MainWindowViewModel vm;
private bool delet = false;
private string target;
public MakeHardLinksCommand(MainWindowViewModel viewmodel, Action<Exception> onException) : base(onException)
{
vm = viewmodel;
vm = viewmodel;
}
protected override async Task ExecuteAsync(object parameter)
{
IsExecuting = true;
vm.UIEnabled = false;
string selInst = vm.SelectedInstance;
var selectedInstance = (from inst in vm.Shared.Info.InstanceList
where inst.InstanceName == selInst
select inst).Single();
vm.LinkButtonEnabled = false;
var prompt = MessageBox.Show($"Creating hard links from {selectedInstance.InstanceName}.\nProceed to link target selection?", "Make hard links", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
var prompt = MessageBox.Show($"Creating hard links from {vm.SelectedInstance.InstanceName}.\nProceed to link target selection?", "Make hard links", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (prompt == DialogResult.Yes)
{
FolderBrowserDialog dialog = new();
if (dialog.ShowDialog() == DialogResult.OK)
{
string target = dialog.SelectedPath;
target = dialog.SelectedPath;
var uniqueFolders = (from folders in selectedInstance.InstanceFiles
where folders.Location != string.Empty
select folders.Location).Distinct();
vm.Loggerr.Log("Checking for duplicates...");
double progressStep = 100D / (selectedInstance.InstanceFiles.Count + uniqueFolders.Count());
bool proceed = await Task.Run(() => CheckDupes());
foreach (var folder in uniqueFolders)
if (delet)
{
if (!Directory.Exists(target + folder))
foreach (var file in vm.SelectedInstance.InstanceFiles)
{
await Task.Run(() => Directory.CreateDirectory(target + folder));
if (File.Exists(target + file.Location + @"\" + file.OriginalFileName))
{
//vm.Loggerr.Log(@$"Deleting {target + file.Location + @"\" + file.OriginalFileName}");
File.Delete(target + file.Location + @"\" + file.OriginalFileName);
}
}
vm.Progress += progressStep;
}
foreach (var file in selectedInstance.InstanceFiles)
vm.Loggerr.Log("Linking...");
if (proceed)
{
if (!File.Exists(target + file.Location + @"\" + file.OriginalFileName))
var uniqueFolders = (from folders in vm.SelectedInstance.InstanceFiles
where folders.Location != string.Empty
select folders.Location).Distinct();
double progressStep = 100D / (vm.SelectedInstance.InstanceFiles.Count + uniqueFolders.Count());
foreach (var folder in uniqueFolders)
{
await Task.Run(() => CreateHardLink(target + file.Location + @"\" + file.OriginalFileName,
dbDir + @"\" + file.HashedFileName, IntPtr.Zero));
if (!Directory.Exists(target + folder))
{
//vm.Loggerr.Log($@"Creating directory {target + folder}");
await Task.Run(() => Directory.CreateDirectory(target + folder));
}
vm.Progress += progressStep;
}
vm.Progress += progressStep;
foreach (var file in vm.SelectedInstance.InstanceFiles)
{
if (!File.Exists(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));
}
vm.Progress += progressStep;
}
}
}
}
vm.Progress = 0;
vm.Loggerr.Log("Link operation finished");
vm.UIEnabled = true;
vm.LinkButtonEnabled = true;
IsExecuting = false;
}
bool CheckDupes()
{
foreach (var file in vm.SelectedInstance.InstanceFiles)
{
if (File.Exists(target + file.Location + @"\" + file.OriginalFileName))
{
var prompt2 = MessageBox.Show(@$"{file.Location}\{file.OriginalFileName} already exists in the target directory!" + "\n"
+ "Do you want to delete all encountered dupes before making hard links? (If you didn't know or forgot, overwriting hard linked files changes the originals). Nothing was linked yet. 'No' cancels the whole operation.", "Duplicate files detected!", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (prompt2 == DialogResult.Yes)
{
delet = true;
return true;
}
else
{
return false;
}
}
}
vm.Progress = 0;
vm.UIEnabled = true;
IsExecuting = false;
return true;
}
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool CreateHardLink(
string lpFileName,
+37 -20
View File
@@ -6,6 +6,7 @@ using System.Data;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
using System.Xml;
@@ -16,7 +17,24 @@ namespace WPFLinkTool
{
public class MainWindowViewModel : ViewModelBase
{
public readonly SharedViewModel Shared;
public SharedViewModel Shared;
public Logger Loggerr;
public ObservableCollection<string> LogEntries
{
get
{
return _logEntries;
}
set
{
_logEntries = value;
OnPropertyChanged(nameof(LogEntries));
}
}
private ObservableCollection<string> _logEntries;
private string _dBSize;
public string DBSize
{
@@ -95,8 +113,8 @@ namespace WPFLinkTool
OnPropertyChanged(nameof(HeaderText));
}
}
private string _selectedInstance;
public string SelectedInstance
private DataInstance _selectedInstance;
public DataInstance SelectedInstance
{
get
{
@@ -106,6 +124,7 @@ namespace WPFLinkTool
{
_selectedInstance = value;
OnPropertyChanged(nameof(SelectedInstance));
LinkButtonEnabled = true;
}
}
@@ -121,25 +140,32 @@ namespace WPFLinkTool
return list;
}
}
public RelayCommand OpenAddInstanceWindow { get; private set; }
public RelayCommand SelectedInstanceCommand { get; private set; }
public RelayCommand DeleteInstanceCommand { get; private set; }
public ICommand MakeHardLinksCommand { get; private set; }
public ICommand DeleteUnusedCommand { get; private set; }
public MainWindowViewModel(SharedViewModel shared)
public ScrollViewer Scroller;
public MainWindowViewModel(SharedViewModel shared, ScrollViewer scroller)
{
if (!Directory.Exists(dbDir))
Directory.CreateDirectory(dbDir);
Shared = shared;
Scroller = scroller;
Loggerr = new(Scroller);
OpenAddInstanceWindow = new(OpenInstanceWindow);
SelectedInstanceCommand = new(SetSelectedRadioButton);
DeleteInstanceCommand = new(DeleteInstance);
MakeHardLinksCommand = new MakeHardLinksCommand(this, (ex) => HeaderText = ex.Message);
DeleteUnusedCommand = new DeleteUnusedCommand(this, (ex) => HeaderText = ex.Message);
MakeHardLinksCommand = new MakeHardLinksCommand(this, (ex) => Loggerr.Log(ex.Message));
DeleteUnusedCommand = new DeleteUnusedCommand(this, (ex) => Loggerr.Log(ex.Message));
LoadDBInfo();
Task.Run(() => UpdateDBSize());
Task.Run(() => UpdateSumSize());
//HeaderText = "Loading finished, probably... Have a nice day!";
LogEntries = Loggerr.LogEntries;
Loggerr.Log("Loading finished, probably... Have a nice day!");
}
private void LoadDBInfo()
@@ -165,28 +191,19 @@ namespace WPFLinkTool
LinkButtonEnabled = false;
Task.Run(() => UpdateDBSize());
Task.Run(() => UpdateSumSize());
}
public void SetSelectedRadioButton(object o)
{
SelectedInstance = (string)o;
LinkButtonEnabled = true;
}
}
public void DeleteInstance(object o)
{
var prompt = MessageBox.Show($"Delete {SelectedInstance}?", "Delete instance", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
var prompt = MessageBox.Show($"Delete {SelectedInstance.InstanceName}? This will not delete the files.", "Delete instance", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (prompt == DialogResult.Yes)
{
DataInstance selected = (from inst in Shared.Info.InstanceList
where inst.InstanceName == SelectedInstance
select inst).Single();
Shared.Info.InstanceList.Remove(selected);
Shared.Info.InstanceList.Remove(SelectedInstance);
Shared.Info.SaveToXml();
LinkButtonEnabled = false;
UpdateSumSize();
OnPropertyChanged(nameof(InstanceList));
Loggerr.Log("Deleting instance operation finished.");
}
}
public void UpdateDBSize()
+3 -6
View File
@@ -8,16 +8,13 @@ namespace WPFLinkTool
{
public class SharedViewModel : ViewModelBase
{
private DBInfo _info;
private DBInfo _Info;
public DBInfo Info
{
get
{
return _info;
}
get { return _Info; }
set
{
_info = value;
_Info = value;
OnPropertyChanged(nameof(Info));
}
}
+7 -1
View File
@@ -2,9 +2,15 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>otoge.ico</ApplicationIcon>
<Version>$(VersionPrefix)</Version>
</PropertyGroup>
<ItemGroup>
<Content Include="otoge.ico" />
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB