Feature/ copy fnc (#1)

Copy Funktion integriert

---------

Co-authored-by: Black_Betty <Black_Betty@DESKTOP-GA9LVM3>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
Black_BettyandBlack_Betty committed 2026-10-06 23:02:38 +02:00
1 parent 976b651e4b
commit 841ba9ed7a
6 files changed
+884 -143

No files matched your search

+322 -123
View File
@@ -15,6 +15,11 @@ 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;
@@ -26,18 +31,75 @@ namespace TapeCatalog
public partial class MainWindow : Window
{
private readonly ObservableCollection<FileEntry> _files = new ObservableCollection<FileEntry>();
private DispatcherTimer _copySummaryTimer;
private static string FormatBytesToMb(long bytes)
// NOTE: FormatBytesToMb moved to Methods.SettingsUi; no local implementation kept.
private void XmlFilesListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// convert to MB with two decimal places
double mb = bytes / 1024.0 / 1024.0;
return mb.ToString("F2");
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)
@@ -82,8 +144,22 @@ namespace TapeCatalog
#region (COPY_TO_TAPECATALOG)
private void COPY_TO_TAPECATALOG(object sender, RoutedEventArgs e)
{
// delegate to XML helper to copy entries to TapeCatalog list view
XML.Methods.CopyToTapeCatalog(this, _files, TapeCatalogListView);
// 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)
@@ -124,165 +200,104 @@ namespace TapeCatalog
private void SET_RES_DIR(object sender, RoutedEventArgs e)
{
// Use the WinForms FolderBrowserDialog to select a folder
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
{
dlg.Description = "Select a folder";
dlg.ShowNewFolderButton = true;
var result = dlg.ShowDialog();
if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
{
SourceTextBox.Text = dlg.SelectedPath;
}
}
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)
{
// Use the WinForms FolderBrowserDialog to select a folder
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
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))
{
dlg.Description = "Select a folder";
dlg.ShowNewFolderButton = true;
var result = dlg.ShowDialog();
if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
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")
{
ArchiveTextBox.Text = dlg.SelectedPath;
MainTabControl.SelectedItem = ti;
break;
}
}
}
#endregion (SET_ARC_DIR)
#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))
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
{
MessageBox.Show("Please select a directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!Directory.Exists(path))
{
MessageBox.Show("The specified directory does not exist.", "Directory not found", MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show("Please select a valid Source directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
try
{
var fi = new FileInfo(file);
// check if an entry with same filename and same source directory already exists
var existing = _files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.FileDir, fi.DirectoryName, StringComparison.OrdinalIgnoreCase));
if (existing != null)
{
var newSizeMb = FormatBytesToMb(fi.Length);
if (existing.SizeSrc != newSizeMb)
{
// update size and modification date, do not add a new entry
existing.SizeSrc = newSizeMb;
existing.ModSrc = fi.LastWriteTime.ToString();
// reset verification status and archive drive info
existing.Status = string.Empty;
existing.ArcDrive = string.Empty;
}
}
else
{
_files.Add(new FileEntry
{
Filename = fi.Name,
FileDir = fi.DirectoryName,
SizeSrc = FormatBytesToMb(fi.Length),
//SizeArc = string.Empty,
ModSrc = fi.LastWriteTime.ToString(),
//ModArc = string.Empty
});
}
}
catch
{
// ignore files we can't access
}
}
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);
}
// inform user that scan finished
MessageBox.Show($"Scan abgeschlossen. Einträge: {_files.Count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
}
#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))
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
{
MessageBox.Show("Please select an archive directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!Directory.Exists(path))
{
MessageBox.Show("The specified archive directory does not exist.", "Directory not found", MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show("Please select a valid Archive directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
try
{
var fi = new FileInfo(file);
// look for existing entry by filename (case-insensitive)
var existing = _files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.Ordinal));
if (existing != null)
{
// update SizeArc
existing.SizeArc = FormatBytesToMb(fi.Length);
// update ModArc to the most recent modified date
DateTime prevMod;
if (!DateTime.TryParse(existing.ModArc, out prevMod))
prevMod = DateTime.MinValue;
var mod = fi.LastWriteTime > prevMod ? fi.LastWriteTime : prevMod;
existing.ModArc = mod.ToString();
}
else
{
_files.Add(new FileEntry
{
Filename = fi.Name,
FileDir = fi.DirectoryName,
//SizeSrc = string.Empty,
SizeArc = FormatBytesToMb(fi.Length),
//ModSrc = string.Empty,
ModArc = fi.LastWriteTime.ToString()
});
}
}
catch
{
// ignore inaccessible files
}
}
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);
}
// inform user that archive scan finished
MessageBox.Show($"Scan abgeschlossen. Einträge: {_files.Count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
}
#endregion(SCAN_ARC_DIR)
#region (CLEAR_FILES)
@@ -341,6 +356,190 @@ namespace TapeCatalog
}
}
}
// 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