SHA256
564 lines
22 KiB
C#
564 lines
22 KiB
C#
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.Navigation;
|
|
using System.Windows.Shapes;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using XML;
|
|
using TapeCatalog.Methods;
|
|
using System.Windows.Threading;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Globalization;
|
|
|
|
|
|
|
|
namespace TapeCatalog
|
|
{
|
|
/// <summary>
|
|
/// Interaction logic for MainWindow.xaml
|
|
/// </summary>
|
|
public partial class MainWindow : Window
|
|
{
|
|
private readonly ObservableCollection<FileEntry> _files = new ObservableCollection<FileEntry>();
|
|
private DispatcherTimer _copySummaryTimer;
|
|
|
|
// NOTE: FormatBytesToMb moved to Methods.SettingsUi; no local implementation kept.
|
|
|
|
private void XmlFilesListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (!(XmlFilesListBox.SelectedItem is string path))
|
|
{
|
|
XmlSummaryListView.Items.Clear();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
XmlSummaryListView.Items.Clear();
|
|
return;
|
|
}
|
|
|
|
var doc = System.Xml.Linq.XDocument.Load(path);
|
|
var root = doc.Root;
|
|
if (root == null)
|
|
{
|
|
XmlSummaryListView.Items.Clear();
|
|
return;
|
|
}
|
|
|
|
XmlSummaryListView.Items.Clear();
|
|
|
|
if (string.Equals(root.Name.LocalName, "Files", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreach (var fe in root.Elements("File"))
|
|
{
|
|
var fn = fe.Element("Filename")?.Value ?? string.Empty;
|
|
var fd = fe.Element("FileDir")?.Value ?? string.Empty;
|
|
var st = fe.Element("Status")?.Value ?? string.Empty;
|
|
XmlSummaryListView.Items.Add(new FileEntry { Filename = fn, FileDir = fd, Status = st });
|
|
}
|
|
}
|
|
else if (string.Equals(root.Name.LocalName, "TapeEntries", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreach (var entry in root.Elements("Entry"))
|
|
{
|
|
var fn = entry.Element("Filename")?.Value ?? string.Empty;
|
|
var med = entry.Element("Medium")?.Value ?? string.Empty;
|
|
var mod = entry.Element("Modified")?.Value ?? string.Empty;
|
|
XmlSummaryListView.Items.Add(new FileEntry { Filename = fn, FileDir = med, Status = mod });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Unknown format - show nothing
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
XmlSummaryListView.Items.Clear();
|
|
}
|
|
}
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
FilesListView.ItemsSource = _files;
|
|
// start periodic update of copy summary
|
|
_copySummaryTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
|
_copySummaryTimer.Tick += (s, e) => UpdateCopySummary();
|
|
_copySummaryTimer.Start();
|
|
}
|
|
#region (export Medium)
|
|
private void EXPORT_MEDIUM(object sender, RoutedEventArgs e)
|
|
{
|
|
var selected = MediumComboBox.SelectedItem as string;
|
|
if (string.IsNullOrWhiteSpace(selected))
|
|
{
|
|
MessageBox.Show(this, "Bitte ein Medium im Dropdown auswählen.", "Keine Auswahl", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
// collect matching entries from TapeCatalogListView
|
|
var matches = new List<XML.Methods.TapeEntry>();
|
|
foreach (var item in TapeCatalogListView.Items)
|
|
{
|
|
var te = item as dynamic;
|
|
if (te == null) continue;
|
|
string m = te.Medium as string;
|
|
if (string.Equals(m, selected, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
matches.Add(new XML.Methods.TapeEntry { Medium = te.Medium, Filename = te.Filename, Modified = te.Modified });
|
|
}
|
|
}
|
|
|
|
if (matches.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "Keine Einträge für das gewählte Medium gefunden.", "Keine Einträge", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
// delegate export to XML helper
|
|
XML.Methods.ExportTapeEntries(this, selected, matches);
|
|
}
|
|
#endregion
|
|
#region (Import List)
|
|
private void IMPORT_LIST(object sender, RoutedEventArgs e)
|
|
{
|
|
// delegate to XML import helper
|
|
XML.Methods.ImportList(this, _files, SourceTextBox, ArchiveTextBox);
|
|
}
|
|
#endregion (Import List)
|
|
#region (COPY_TO_TAPECATALOG)
|
|
private void COPY_TO_TAPECATALOG(object sender, RoutedEventArgs e)
|
|
{
|
|
// Only copy entries where the verification status indicates OK (checked)
|
|
try
|
|
{
|
|
var okEntries = _files.Where(f => string.Equals(f.Status, "✔", StringComparison.Ordinal)).ToList();
|
|
if (okEntries.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "No verified entries to copy to TapeCatalog.", "Nothing to copy", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
XML.Methods.CopyToTapeCatalog(this, okEntries, TapeCatalogListView);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, $"Error copying to TapeCatalog: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
#endregion (COPY_TO_TAPECATALOG)
|
|
#region (COPY_FROM_SRCARC)
|
|
private void COPY_FROM_SRCARC(object sender, RoutedEventArgs e)
|
|
{
|
|
XML.Methods.CopyToTapeCatalog(this, _files, TapeCatalogListView);
|
|
}
|
|
#endregion (COPY_FROM_SRCARC)
|
|
#region (REFRESH_MEDIUMS)
|
|
private void REFRESH_MEDIUMS(object sender, RoutedEventArgs e)
|
|
{
|
|
// collect distinct mediums from TapeCatalogListView items
|
|
try
|
|
{
|
|
var mediums = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in TapeCatalogListView.Items)
|
|
{
|
|
var te = item as dynamic;
|
|
if (te == null) continue;
|
|
string m = te.Medium as string;
|
|
if (!string.IsNullOrWhiteSpace(m)) mediums.Add(m);
|
|
}
|
|
|
|
MediumComboBox.ItemsSource = mediums.OrderBy(s => s).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, $"Fehler beim Aktualisieren der Medium-Liste: {ex.Message}", "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
#endregion (REFRESH_MEDIUMS)
|
|
#region (EXPORT_LIST)
|
|
private void EXPORT_LIST(object sender, RoutedEventArgs e)
|
|
{
|
|
// Delegate export to XML helper
|
|
XML.Methods.ExportList(this, SourceTextBox.Text, ArchiveTextBox.Text, _files);
|
|
}
|
|
|
|
private void SET_RES_DIR(object sender, RoutedEventArgs e)
|
|
{
|
|
var sel = SettingsUi.SelectDirectory("Select Source folder");
|
|
if (!string.IsNullOrWhiteSpace(sel)) SourceTextBox.Text = sel;
|
|
}
|
|
#endregion (EXPORT_LIST)
|
|
#region (SET_ARC_DIR)
|
|
private void SET_ARC_DIR(object sender, RoutedEventArgs e)
|
|
{
|
|
var sel = SettingsUi.SelectDirectory("Select Archive folder");
|
|
if (!string.IsNullOrWhiteSpace(sel)) ArchiveTextBox.Text = sel;
|
|
}
|
|
#endregion (SET_ARC_DIR)
|
|
#region (SET_XML_DIR)
|
|
private void SET_XML_DIR(object sender, RoutedEventArgs e)
|
|
{
|
|
var sel = SettingsUi.SelectDirectory("Select XML folder");
|
|
if (!string.IsNullOrWhiteSpace(sel)) XmlDirectoryTextBox.Text = sel;
|
|
}
|
|
#endregion (SET_XML_DIR)
|
|
|
|
#region (LOAD_XML_FILES)
|
|
private void LOAD_XML_FILES(object sender, RoutedEventArgs e)
|
|
{
|
|
var xmlPath = XmlDirectoryTextBox.Text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(xmlPath) || !Directory.Exists(xmlPath))
|
|
{
|
|
MessageBox.Show(this, "Please select a valid XML directory first.", "Invalid XML Directory", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
string[] files;
|
|
try
|
|
{
|
|
files = Directory.GetFiles(xmlPath, "*.xml", SearchOption.TopDirectoryOnly);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, $"Error enumerating XML files: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
XmlFilesListBox.Items.Clear();
|
|
XmlSummaryListView.Items.Clear();
|
|
foreach (var f in files)
|
|
{
|
|
XmlFilesListBox.Items.Add(f);
|
|
}
|
|
|
|
// switch to XML tab
|
|
foreach (var it in MainTabControl.Items)
|
|
{
|
|
if (it is TabItem ti && (ti.Header as string) == "XML")
|
|
{
|
|
MainTabControl.SelectedItem = ti;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endregion (LOAD_XML_FILES)
|
|
#region (SCAN_RES_DIR)
|
|
private void SCAN_RES_DIR(object sender, RoutedEventArgs e)
|
|
{
|
|
var path = SourceTextBox.Text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
|
|
{
|
|
MessageBox.Show("Please select a valid Source directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var count = SettingsUi.ScanSource(path, _files);
|
|
MessageBox.Show($"Scan abgeschlossen. Einträge: {count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Error scanning directory: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
#endregion (SCAN_RES_DIR)
|
|
#region(SCAN_ARC_DIR)
|
|
private void SCAN_ARC_DIR(object sender, RoutedEventArgs e)
|
|
{
|
|
var path = ArchiveTextBox.Text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
|
|
{
|
|
MessageBox.Show("Please select a valid Archive directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var count = SettingsUi.ScanArchive(path, _files);
|
|
MessageBox.Show($"Scan abgeschlossen. Einträge: {count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Error scanning archive directory: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
#endregion(SCAN_ARC_DIR)
|
|
#region (CLEAR_FILES)
|
|
private void CLEAR_FILES(object sender, RoutedEventArgs e)
|
|
{
|
|
_files.Clear();
|
|
}
|
|
#endregion (CLEAR_FILES)
|
|
#region (VERIFY_SIZES)
|
|
private void VERIFY_SIZES(object sender, RoutedEventArgs e)
|
|
{
|
|
// determine archive drive name (volume label or drive root)
|
|
string archivePath = ArchiveTextBox.Text?.Trim();
|
|
string archiveDriveName = string.Empty;
|
|
if (!string.IsNullOrWhiteSpace(archivePath))
|
|
{
|
|
try
|
|
{
|
|
var root = System.IO.Path.GetPathRoot(archivePath);
|
|
if (!string.IsNullOrWhiteSpace(root))
|
|
{
|
|
try
|
|
{
|
|
var di = new DriveInfo(root);
|
|
archiveDriveName = string.IsNullOrWhiteSpace(di.VolumeLabel) ? root.TrimEnd('\\') : di.VolumeLabel;
|
|
}
|
|
catch
|
|
{
|
|
archiveDriveName = root.TrimEnd('\\');
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
archiveDriveName = string.Empty;
|
|
}
|
|
}
|
|
|
|
foreach (var entry in _files)
|
|
{
|
|
// Sizes are stored in MB with two decimals; compare as doubles
|
|
double src = 0;
|
|
double arc = 0;
|
|
double.TryParse(entry.SizeSrc, out src);
|
|
double.TryParse(entry.SizeArc, out arc);
|
|
|
|
if (src > 0 && arc > 0 && Math.Abs(src - arc) < 0.01)
|
|
{
|
|
entry.Status = "✔";
|
|
entry.ArcDrive = archiveDriveName;
|
|
}
|
|
else
|
|
{
|
|
entry.Status = "✖";
|
|
entry.ArcDrive = string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate CopyTreeView with directory structure from SourceTextBox
|
|
private void POPULATE_COPY_TREE(object sender, RoutedEventArgs e)
|
|
{
|
|
Copy.PopulateTree(CopyTreeView, SourceTextBox.Text);
|
|
}
|
|
|
|
private async void COPY_SELECTED_TO_ARCHIVE(object sender, RoutedEventArgs e)
|
|
{
|
|
var archivePath = ArchiveTextBox.Text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(archivePath) || !Directory.Exists(archivePath))
|
|
{
|
|
MessageBox.Show(this, "Please select a valid Archive directory first.", "Invalid Archive", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
var sourceRoot = SourceTextBox.Text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(sourceRoot) || !Directory.Exists(sourceRoot))
|
|
{
|
|
MessageBox.Show(this, "Please select a valid Source directory first.", "Invalid Source", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
var files = Copy.GetCheckedFiles(CopyTreeView);
|
|
if (files.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "No files selected.", "Nothing to copy", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
// prepare progress UI based on bytes
|
|
long totalBytes = 0;
|
|
foreach (var f in files)
|
|
{
|
|
try { totalBytes += new FileInfo(f).Length; } catch { }
|
|
}
|
|
|
|
if (totalBytes <= 0) totalBytes = 1; // avoid zero maximum
|
|
|
|
CopyProgressBar.Minimum = 0;
|
|
CopyProgressBar.Maximum = (double)totalBytes;
|
|
CopyProgressBar.Value = 0;
|
|
CopyProgressText.Text = "Starting...";
|
|
|
|
long bytesCopied = 0;
|
|
int copied = 0, failed = 0;
|
|
|
|
// parse speed limit (MB/s) from settings; 0 or invalid = unlimited
|
|
double speedMb = 0;
|
|
var speedText = CopySpeedTextBox?.Text?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(speedText))
|
|
double.TryParse(speedText, NumberStyles.Float, CultureInfo.InvariantCulture, out speedMb);
|
|
|
|
double speedBytesPerSec = speedMb > 0 ? speedMb * 1024.0 * 1024.0 : 0;
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
const int bufferSize = 64 * 1024; // 64KB
|
|
var buffer = new byte[bufferSize];
|
|
|
|
for (int i = 0; i < files.Count; i++)
|
|
{
|
|
var src = files[i];
|
|
try
|
|
{
|
|
var fileDir = System.IO.Path.GetDirectoryName(src) ?? string.Empty;
|
|
string relative;
|
|
if (!string.IsNullOrWhiteSpace(sourceRoot) && fileDir.StartsWith(sourceRoot, StringComparison.OrdinalIgnoreCase))
|
|
relative = fileDir.Substring(sourceRoot.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
|
else
|
|
relative = System.IO.Path.GetFileName(fileDir);
|
|
|
|
var destDir = System.IO.Path.Combine(archivePath, relative);
|
|
Directory.CreateDirectory(destDir);
|
|
var destFile = System.IO.Path.Combine(destDir, System.IO.Path.GetFileName(src));
|
|
|
|
using (var inStream = new FileStream(src, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
using (var outStream = new FileStream(destFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
|
{
|
|
long fileBytesCopied = 0;
|
|
var sw = Stopwatch.StartNew();
|
|
int read;
|
|
while ((read = inStream.Read(buffer, 0, buffer.Length)) > 0)
|
|
{
|
|
outStream.Write(buffer, 0, read);
|
|
fileBytesCopied += read;
|
|
Interlocked.Add(ref bytesCopied, read);
|
|
|
|
// throttling per file
|
|
if (speedBytesPerSec > 0)
|
|
{
|
|
double desiredMs = (fileBytesCopied / speedBytesPerSec) * 1000.0;
|
|
double actualMs = sw.Elapsed.TotalMilliseconds;
|
|
if (desiredMs > actualMs)
|
|
{
|
|
int sleep = (int)Math.Ceiling(desiredMs - actualMs);
|
|
if (sleep > 0) Thread.Sleep(sleep);
|
|
}
|
|
}
|
|
|
|
// update UI progress
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
CopyProgressBar.Value = Math.Min((double)bytesCopied, CopyProgressBar.Maximum);
|
|
double pct = (CopyProgressBar.Value / CopyProgressBar.Maximum) * 100.0;
|
|
CopyProgressText.Text = $"{FormatBytes(bytesCopied)} / {FormatBytes(totalBytes)} ({pct:F1}%)";
|
|
});
|
|
}
|
|
}
|
|
|
|
copied++;
|
|
}
|
|
catch
|
|
{
|
|
failed++;
|
|
}
|
|
}
|
|
});
|
|
|
|
CopyProgressText.Text = $"Done. Success: {copied}, Failed: {failed}. {FormatBytes(bytesCopied)} copied.";
|
|
MessageBox.Show(this, $"Copy finished. Success: {copied}, Failed: {failed}", "Copy completed", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
|
|
private void UpdateCopySummary()
|
|
{
|
|
try
|
|
{
|
|
var files = Copy.GetCheckedFiles(CopyTreeView);
|
|
CopySelectedCountText.Text = files.Count.ToString();
|
|
|
|
double totalMb = 0;
|
|
foreach (var f in files)
|
|
{
|
|
try
|
|
{
|
|
var fi = new FileInfo(f);
|
|
totalMb += fi.Length / 1024.0 / 1024.0;
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
CopySelectedSizeText.Text = totalMb.ToString("F2");
|
|
|
|
var archivePath = ArchiveTextBox.Text?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(archivePath))
|
|
{
|
|
try
|
|
{
|
|
var root = System.IO.Path.GetPathRoot(archivePath);
|
|
if (!string.IsNullOrWhiteSpace(root))
|
|
{
|
|
var di = new DriveInfo(root);
|
|
// AvailableFreeSpace in bytes
|
|
double freeMb = di.AvailableFreeSpace / 1024.0 / 1024.0;
|
|
ArchiveFreeText.Text = freeMb.ToString("F2") + " MB";
|
|
}
|
|
else
|
|
{
|
|
ArchiveFreeText.Text = "N/A";
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
ArchiveFreeText.Text = "N/A";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ArchiveFreeText.Text = "N/A";
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// swallow periodic update errors; avoid UI disruptions
|
|
}
|
|
}
|
|
|
|
private static string FormatBytes(long bytes)
|
|
{
|
|
double mb = bytes / 1024.0 / 1024.0;
|
|
return mb.ToString("F2") + " MB";
|
|
}
|
|
}
|
|
#endregion (VERIFY_SIZES)
|
|
public class FileEntry : System.ComponentModel.INotifyPropertyChanged
|
|
{
|
|
public string Filename { get; set; }
|
|
public string FileDir { get; set; }
|
|
private string _sizeSrc;
|
|
public string SizeSrc { get => _sizeSrc; set { _sizeSrc = value; OnPropertyChanged(nameof(SizeSrc)); } }
|
|
private string _sizeArc;
|
|
public string SizeArc { get => _sizeArc; set { _sizeArc = value; OnPropertyChanged(nameof(SizeArc)); } }
|
|
public string ModSrc { get; set; }
|
|
public string ModArc { get; set; }
|
|
private string _status;
|
|
public string Status { get => _status; set { _status = value; OnPropertyChanged(nameof(Status)); } }
|
|
private string _arcDrive;
|
|
public string ArcDrive { get => _arcDrive; set { _arcDrive = value; OnPropertyChanged(nameof(ArcDrive)); } }
|
|
|
|
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
|
|
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propName));
|
|
}
|
|
}
|