SHA256
Compare commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
841ba9ed7a |
No files matched your search
+85
-20
@@ -7,21 +7,15 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="450" Width="800">
|
Title="MainWindow" Height="450" Width="800">
|
||||||
<Grid>
|
<Grid>
|
||||||
<TabControl>
|
<TabControl x:Name="MainTabControl">
|
||||||
<TabItem Header="SRC-ARC">
|
<TabItem Header="SRC-ARC">
|
||||||
<Grid Background="#FFE5E5E5">
|
<Grid Background="#FFE5E5E5">
|
||||||
<TextBox x:Name="SourceTextBox" HorizontalAlignment="Left" Margin="0,19,0,0" TextWrapping="Wrap" Text="Source Directory" VerticalAlignment="Top" Width="507" Height="20"/>
|
<Button Content="Clear List" HorizontalAlignment="Right" Height="22" Margin="0,10,10,0" VerticalAlignment="Top" Width="76" Click="CLEAR_FILES" />
|
||||||
<TextBox x:Name="ArchiveTextBox" HorizontalAlignment="Left" Height="20" Margin="0,56,0,0" TextWrapping="Wrap" Text="Archive Directory" VerticalAlignment="Top" Width="507"/>
|
<Button Content="Verify Sizes" HorizontalAlignment="Right" Height="22" Margin="0,40,10,0" VerticalAlignment="Top" Width="76" Click="VERIFY_SIZES" />
|
||||||
<Button x:Name="SourceSelect" Content="Select Directory" HorizontalAlignment="Left" Margin="512,19,0,0" VerticalAlignment="Top" Height="20" Click="SET_RES_DIR"/>
|
<Button Content="Export XML" HorizontalAlignment="Right" Height="22" Margin="0,70,10,0" VerticalAlignment="Top" Width="76" Click="EXPORT_LIST" />
|
||||||
<Button x:Name="ArchiveSelect" Content="Select Directory" HorizontalAlignment="Left" Margin="512,56,0,0" VerticalAlignment="Top" Height="20" Click="SET_ARC_DIR"/>
|
<Button Content="Import XML" HorizontalAlignment="Right" Height="22" Margin="0,100,10,0" VerticalAlignment="Top" Width="76" Click="IMPORT_LIST" />
|
||||||
<Button Content="Scan Source" HorizontalAlignment="Left" Height="22" Margin="700,146,0,0" VerticalAlignment="Top" Width="76" Click="SCAN_RES_DIR" RenderTransformOrigin="2.316,5.185" Grid.Column="1"/>
|
<Button Content="-> TapeCat" HorizontalAlignment="Right" Height="22" Margin="0,130,10,0" VerticalAlignment="Top" Width="76" Click="COPY_TO_TAPECATALOG" />
|
||||||
<Button Content="Scan Archive" HorizontalAlignment="Left" Height="22" Margin="700,168,0,0" VerticalAlignment="Top" Width="76" Click="SCAN_ARC_DIR" Grid.Column="1"/>
|
<ListView x:Name="FilesListView" Margin="10,10,101,10" >
|
||||||
<Button Content="Clear List" HorizontalAlignment="Left" Height="22" Margin="700,190,0,0" VerticalAlignment="Top" Width="76" Click="CLEAR_FILES" Grid.Column="1"/>
|
|
||||||
<Button Content="Verify Sizes" HorizontalAlignment="Left" Height="22" Margin="700,212,0,0" VerticalAlignment="Top" Width="76" Click="VERIFY_SIZES" Grid.Column="1"/>
|
|
||||||
<Button Content="Export XML" HorizontalAlignment="Left" Height="22" Margin="700,234,0,0" VerticalAlignment="Top" Width="76" Click="EXPORT_LIST" Grid.Column="1"/>
|
|
||||||
<Button Content="Import XML" HorizontalAlignment="Left" Height="22" Margin="700,256,0,0" VerticalAlignment="Top" Width="76" Click="IMPORT_LIST" Grid.Column="1"/>
|
|
||||||
<Button Content="-> TapeCat" HorizontalAlignment="Left" Height="22" Margin="700,278,0,0" VerticalAlignment="Top" Width="76" Click="COPY_TO_TAPECATALOG" Grid.Column="1"/>
|
|
||||||
<ListView x:Name="FilesListView" Margin="10,146,101,10" Grid.ColumnSpan="2">
|
|
||||||
<ListView.View>
|
<ListView.View>
|
||||||
<GridView>
|
<GridView>
|
||||||
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="300"/>
|
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="300"/>
|
||||||
@@ -35,15 +29,14 @@
|
|||||||
</GridView>
|
</GridView>
|
||||||
</ListView.View>
|
</ListView.View>
|
||||||
</ListView>
|
</ListView>
|
||||||
<TextBlock TextWrapping="Wrap" Text="Source Directory:" Margin="0,0,697,387"/>
|
|
||||||
<TextBlock TextWrapping="Wrap" Text="Archive Directory:" Margin="0,37,697,350"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem Header="Tapecatalog">
|
<TabItem Header="Tapecatalog">
|
||||||
<Grid Background="#FFE5E5E5">
|
<Grid Background="#FFE5E5E5">
|
||||||
<ComboBox x:Name="MediumComboBox" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="200" Grid.ColumnSpan="2"/>
|
<ComboBox x:Name="MediumComboBox" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="200" />
|
||||||
<Button Content="Refresh Mediums" HorizontalAlignment="Left" Height="22" Margin="218,10,0,0" VerticalAlignment="Top" Width="120" Click="REFRESH_MEDIUMS"/>
|
<Button Content="Refresh Mediums" HorizontalAlignment="Left" Height="22" Margin="220,10,0,0" VerticalAlignment="Top" Width="120" Click="REFRESH_MEDIUMS" />
|
||||||
<Button Content="Copy from SRC-ARC" HorizontalAlignment="Left" Height="22" Margin="343,10,0,0" VerticalAlignment="Top" Width="120" Click="COPY_FROM_SRCARC"/>
|
<Button Content="Copy from SRC-ARC" HorizontalAlignment="Left" Height="22" Margin="350,10,0,0" VerticalAlignment="Top" Width="120" Click="COPY_FROM_SRCARC" />
|
||||||
|
<Button Content="Export Medium" HorizontalAlignment="Left" Height="22" Margin="480,10,0,0" VerticalAlignment="Top" Width="120" Click="EXPORT_MEDIUM" />
|
||||||
<ListView x:Name="TapeCatalogListView" Margin="10,44,10,40" Grid.ColumnSpan="2">
|
<ListView x:Name="TapeCatalogListView" Margin="10,44,10,40" Grid.ColumnSpan="2">
|
||||||
<ListView.View>
|
<ListView.View>
|
||||||
<GridView>
|
<GridView>
|
||||||
@@ -53,10 +46,82 @@
|
|||||||
</GridView>
|
</GridView>
|
||||||
</ListView.View>
|
</ListView.View>
|
||||||
</ListView>
|
</ListView>
|
||||||
<Button Content="Export Medium" HorizontalAlignment="Left" Height="22" Margin="468,0,0,374" VerticalAlignment="Bottom" Width="120" Click="EXPORT_MEDIUM"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
<TabItem Header="Copy">
|
||||||
|
<Grid Background="#FFE5E5E5">
|
||||||
|
<Button Content="Refresh Copy Tree" HorizontalAlignment="Left" Height="22" Margin="10,10,0,0" VerticalAlignment="Top" Width="140" Click="POPULATE_COPY_TREE"/>
|
||||||
|
<Button Content="Copy Selected to Archive" HorizontalAlignment="Left" Height="22" Margin="160,10,0,0" VerticalAlignment="Top" Width="180" Click="COPY_SELECTED_TO_ARCHIVE" />
|
||||||
|
<StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="370,10,0,0" Width="210">
|
||||||
|
<TextBlock Text="Selected files:"/>
|
||||||
|
<TextBlock x:Name="CopySelectedCountText" Text="0" Margin="0,2,0,6"/>
|
||||||
|
<TextBlock Text="Selected size (MB):"/>
|
||||||
|
<TextBlock x:Name="CopySelectedSizeText" Text="0.00" Margin="0,2,0,6"/>
|
||||||
|
<TextBlock Text="Archive free:"/>
|
||||||
|
<TextBlock x:Name="ArchiveFreeText" Text="-" Margin="0,2,0,6"/>
|
||||||
|
<ProgressBar x:Name="CopyProgressBar" Width="200" Height="16" Minimum="0" Maximum="100" Value="0" Margin="0,6,0,0" />
|
||||||
|
<TextBlock x:Name="CopyProgressText" Text="" Margin="0,2,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
<TreeView x:Name="CopyTreeView" Margin="10,170,10,10" Grid.ColumnSpan="2" />
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="XML">
|
||||||
|
<Grid Background="#FFE5E5E5">
|
||||||
|
<ListBox x:Name="XmlFilesListBox" SelectionChanged="XmlFilesListBox_SelectionChanged" Width="200" Margin="10,10,590,10"/>
|
||||||
|
<ListView x:Name="XmlSummaryListView" Width="570" Margin="220,10,4,0">
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="200" />
|
||||||
|
<GridViewColumn Header="FileDir" DisplayMemberBinding="{Binding FileDir}" Width="300" />
|
||||||
|
<GridViewColumn Header="Status" DisplayMemberBinding="{Binding Status}" Width="100" />
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="Settings">
|
||||||
|
<Grid Background="#FFE5E5E5" Margin="10,0,0,0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="150" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="Application Settings" FontWeight="Bold" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Margin="6" />
|
||||||
|
|
||||||
|
<TextBlock Text="Source Directory:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="6" />
|
||||||
|
<TextBox x:Name="SourceTextBox" Grid.Row="1" Grid.Column="1" Height="22" Margin="6" Text="Source Directory" />
|
||||||
|
<Button x:Name="SourceSelect" Content="Select Directory" Grid.Row="1" Grid.Column="2" Click="SET_RES_DIR" Margin="6" Height="22" />
|
||||||
|
|
||||||
|
<TextBlock Text="Archive Directory:" Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="6" />
|
||||||
|
<TextBox x:Name="ArchiveTextBox" Grid.Row="2" Grid.Column="1" Height="22" Margin="6" Text="Archive Directory" />
|
||||||
|
<Button x:Name="ArchiveSelect" Content="Select Directory" Grid.Row="2" Grid.Column="2" Click="SET_ARC_DIR" Margin="6" Height="22" />
|
||||||
|
|
||||||
|
<TextBlock Text="XML Directory:" Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="6" />
|
||||||
|
<TextBox x:Name="XmlDirectoryTextBox" Grid.Row="3" Grid.Column="1" Height="22" Margin="6" Text="XML Directory" />
|
||||||
|
<Button x:Name="XmlDirectorySelect" Content="Select Directory" Grid.Row="3" Grid.Column="2" Click="SET_XML_DIR" Margin="6" Height="22" />
|
||||||
|
|
||||||
|
<TextBlock Text="Copy speed (MB/s):" Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="6" />
|
||||||
|
<TextBox x:Name="CopySpeedTextBox" Grid.Row="4" Grid.Column="1" Height="22" Margin="6" Text="0" />
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Grid.Row="5" Grid.Column="1" Margin="6">
|
||||||
|
<Button Content="Scan Source" Click="SCAN_RES_DIR" Margin="0,0,6,0" Height="24" />
|
||||||
|
<Button Content="Scan Archive" Click="SCAN_ARC_DIR" Margin="0,0,6,0" Height="24" />
|
||||||
|
<Button Content="Load XML Files" Click="LOAD_XML_FILES" Height="24" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
+322
-123
@@ -15,6 +15,11 @@ using System.Windows.Shapes;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using XML;
|
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
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly ObservableCollection<FileEntry> _files = new ObservableCollection<FileEntry>();
|
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
|
if (!(XmlFilesListBox.SelectedItem is string path))
|
||||||
double mb = bytes / 1024.0 / 1024.0;
|
{
|
||||||
return mb.ToString("F2");
|
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()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
FilesListView.ItemsSource = _files;
|
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)
|
#region (export Medium)
|
||||||
private void EXPORT_MEDIUM(object sender, RoutedEventArgs e)
|
private void EXPORT_MEDIUM(object sender, RoutedEventArgs e)
|
||||||
@@ -82,8 +144,22 @@ namespace TapeCatalog
|
|||||||
#region (COPY_TO_TAPECATALOG)
|
#region (COPY_TO_TAPECATALOG)
|
||||||
private void COPY_TO_TAPECATALOG(object sender, RoutedEventArgs e)
|
private void COPY_TO_TAPECATALOG(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// delegate to XML helper to copy entries to TapeCatalog list view
|
// Only copy entries where the verification status indicates OK (checked)
|
||||||
XML.Methods.CopyToTapeCatalog(this, _files, TapeCatalogListView);
|
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)
|
#endregion (COPY_TO_TAPECATALOG)
|
||||||
#region (COPY_FROM_SRCARC)
|
#region (COPY_FROM_SRCARC)
|
||||||
@@ -124,165 +200,104 @@ namespace TapeCatalog
|
|||||||
|
|
||||||
private void SET_RES_DIR(object sender, RoutedEventArgs e)
|
private void SET_RES_DIR(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Use the WinForms FolderBrowserDialog to select a folder
|
var sel = SettingsUi.SelectDirectory("Select Source folder");
|
||||||
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
|
if (!string.IsNullOrWhiteSpace(sel)) SourceTextBox.Text = sel;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endregion (EXPORT_LIST)
|
#endregion (EXPORT_LIST)
|
||||||
#region (SET_ARC_DIR)
|
#region (SET_ARC_DIR)
|
||||||
private void SET_ARC_DIR(object sender, RoutedEventArgs e)
|
private void SET_ARC_DIR(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Use the WinForms FolderBrowserDialog to select a folder
|
var sel = SettingsUi.SelectDirectory("Select Archive folder");
|
||||||
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
|
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";
|
MessageBox.Show(this, "Please select a valid XML directory first.", "Invalid XML Directory", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
dlg.ShowNewFolderButton = true;
|
return;
|
||||||
var result = dlg.ShowDialog();
|
}
|
||||||
if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
|
|
||||||
|
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)
|
#region (SCAN_RES_DIR)
|
||||||
private void SCAN_RES_DIR(object sender, RoutedEventArgs e)
|
private void SCAN_RES_DIR(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var path = SourceTextBox.Text?.Trim();
|
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);
|
MessageBox.Show("Please select a valid Source 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
var count = SettingsUi.ScanSource(path, _files);
|
||||||
{
|
MessageBox.Show($"Scan abgeschlossen. Einträge: {count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Error scanning directory: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
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)
|
#endregion (SCAN_RES_DIR)
|
||||||
#region(SCAN_ARC_DIR)
|
#region(SCAN_ARC_DIR)
|
||||||
private void SCAN_ARC_DIR(object sender, RoutedEventArgs e)
|
private void SCAN_ARC_DIR(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var path = ArchiveTextBox.Text?.Trim();
|
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);
|
MessageBox.Show("Please select a valid 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
var count = SettingsUi.ScanArchive(path, _files);
|
||||||
{
|
MessageBox.Show($"Scan abgeschlossen. Einträge: {count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Error scanning archive directory: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
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)
|
#endregion(SCAN_ARC_DIR)
|
||||||
#region (CLEAR_FILES)
|
#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)
|
#endregion (VERIFY_SIZES)
|
||||||
public class FileEntry : System.ComponentModel.INotifyPropertyChanged
|
public class FileEntry : System.ComponentModel.INotifyPropertyChanged
|
||||||
|
|||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace TapeCatalog.Methods
|
||||||
|
{
|
||||||
|
// Reusable helper methods for copy-related UI functionality
|
||||||
|
public static class Copy
|
||||||
|
{
|
||||||
|
// guard to prevent recursive Checked/Unchecked handling while we programmatically change states
|
||||||
|
private static bool _suppressCheckEvents = false;
|
||||||
|
public static void PopulateTree(TreeView tree, string path)
|
||||||
|
{
|
||||||
|
if (tree == null) return;
|
||||||
|
tree.Items.Clear();
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)) return;
|
||||||
|
|
||||||
|
var rootDir = new DirectoryInfo(path);
|
||||||
|
var rootItem = CreateDirectoryNode(rootDir);
|
||||||
|
rootItem.IsExpanded = true;
|
||||||
|
tree.Items.Add(rootItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TreeViewItem CreateDirectoryNode(DirectoryInfo dir)
|
||||||
|
{
|
||||||
|
var item = new TreeViewItem { Tag = dir.FullName };
|
||||||
|
|
||||||
|
// Create header with a checkbox for the directory as well
|
||||||
|
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
var dirCb = new CheckBox { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) };
|
||||||
|
var dirTb = new TextBlock { Text = dir.Name, VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
headerPanel.Children.Add(dirCb);
|
||||||
|
headerPanel.Children.Add(dirTb);
|
||||||
|
item.Header = headerPanel;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// add subdirectories
|
||||||
|
foreach (var sub in dir.GetDirectories())
|
||||||
|
{
|
||||||
|
item.Items.Add(CreateDirectoryNode(sub));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add files
|
||||||
|
foreach (var f in dir.GetFiles())
|
||||||
|
{
|
||||||
|
var fileItem = new TreeViewItem { Tag = f.FullName };
|
||||||
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
var cb = new CheckBox { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) };
|
||||||
|
var tb = new TextBlock { Text = f.Name, VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
sp.Children.Add(cb);
|
||||||
|
sp.Children.Add(tb);
|
||||||
|
fileItem.Header = sp;
|
||||||
|
item.Items.Add(fileItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore access exceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire events to propagate state to children when directory checkbox is toggled
|
||||||
|
dirCb.Checked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_suppressCheckEvents) return;
|
||||||
|
try { _suppressCheckEvents = true; SetCheckedState(item, true); }
|
||||||
|
finally { _suppressCheckEvents = false; }
|
||||||
|
};
|
||||||
|
dirCb.Unchecked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_suppressCheckEvents) return;
|
||||||
|
try { _suppressCheckEvents = true; SetCheckedState(item, false); }
|
||||||
|
finally { _suppressCheckEvents = false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetCheckedState(TreeViewItem node, bool isChecked)
|
||||||
|
{
|
||||||
|
foreach (var it in node.Items)
|
||||||
|
{
|
||||||
|
if (it is TreeViewItem child)
|
||||||
|
{
|
||||||
|
// child header expected to be a StackPanel with a CheckBox as first child
|
||||||
|
if (child.Header is StackPanel sp && sp.Children.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var ch in sp.Children)
|
||||||
|
{
|
||||||
|
if (ch is CheckBox childCb)
|
||||||
|
{
|
||||||
|
childCb.IsChecked = isChecked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recurse into grandchildren
|
||||||
|
if (child.Items != null && child.Items.Count > 0)
|
||||||
|
{
|
||||||
|
SetCheckedState(child, isChecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect checked files and copy them to archive preserving relative folders
|
||||||
|
public static Tuple<int,int> CopySelectedToArchive(TreeView tree, string sourceRoot, string archivePath)
|
||||||
|
{
|
||||||
|
if (tree == null) return Tuple.Create(0, 0);
|
||||||
|
var files = new List<string>();
|
||||||
|
foreach (var it in tree.Items)
|
||||||
|
{
|
||||||
|
if (it is TreeViewItem tvi)
|
||||||
|
CollectCheckedFiles(tvi, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
int copied = 0, failed = 0;
|
||||||
|
foreach (var src in files)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
File.Copy(src, destFile, true);
|
||||||
|
copied++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tuple.Create(copied, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a list of all checked file paths in the tree
|
||||||
|
public static List<string> GetCheckedFiles(TreeView tree)
|
||||||
|
{
|
||||||
|
var files = new List<string>();
|
||||||
|
if (tree == null) return files;
|
||||||
|
foreach (var it in tree.Items)
|
||||||
|
{
|
||||||
|
if (it is TreeViewItem tvi)
|
||||||
|
CollectCheckedFiles(tvi, files);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectCheckedFiles(TreeViewItem node, List<string> list)
|
||||||
|
{
|
||||||
|
if (node.Header is StackPanel sp)
|
||||||
|
{
|
||||||
|
foreach (var child in sp.Children)
|
||||||
|
{
|
||||||
|
if (child is CheckBox cb && cb.IsChecked == true)
|
||||||
|
{
|
||||||
|
if (node.Tag is string path && File.Exists(path))
|
||||||
|
{
|
||||||
|
list.Add(path);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var it in node.Items)
|
||||||
|
{
|
||||||
|
if (it is TreeViewItem t)
|
||||||
|
CollectCheckedFiles(t, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace TapeCatalog.Methods
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simple settings container and load/save helpers.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class SettingsData
|
||||||
|
{
|
||||||
|
public string SourceDirectory { get; set; }
|
||||||
|
public string ArchiveDirectory { get; set; }
|
||||||
|
public string XmlDirectory { get; set; }
|
||||||
|
|
||||||
|
public static SettingsData Default => new SettingsData
|
||||||
|
{
|
||||||
|
SourceDirectory = string.Empty,
|
||||||
|
ArchiveDirectory = string.Empty,
|
||||||
|
XmlDirectory = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SettingsManager
|
||||||
|
{
|
||||||
|
private const string DefaultFileName = "appsettings.xml";
|
||||||
|
|
||||||
|
public static void Save(SettingsData data, string filePath = null)
|
||||||
|
{
|
||||||
|
if (data == null) throw new ArgumentNullException(nameof(data));
|
||||||
|
var path = string.IsNullOrWhiteSpace(filePath) ? DefaultFileName : filePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var fs = File.Create(path))
|
||||||
|
{
|
||||||
|
var ser = new XmlSerializer(typeof(SettingsData));
|
||||||
|
ser.Serialize(fs, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// swallow errors; callers can handle existence/permission issues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SettingsData Load(string filePath = null)
|
||||||
|
{
|
||||||
|
var path = string.IsNullOrWhiteSpace(filePath) ? DefaultFileName : filePath;
|
||||||
|
if (!File.Exists(path)) return SettingsData.Default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var fs = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
var ser = new XmlSerializer(typeof(SettingsData));
|
||||||
|
return ser.Deserialize(fs) as SettingsData ?? SettingsData.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return SettingsData.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI-related helpers used by the Settings tab. These methods avoid direct UI manipulation
|
||||||
|
// and return data so the MainWindow can update controls.
|
||||||
|
public static class SettingsUi
|
||||||
|
{
|
||||||
|
public static string SelectDirectory(string description)
|
||||||
|
{
|
||||||
|
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
|
||||||
|
{
|
||||||
|
dlg.Description = description ?? "Select a folder";
|
||||||
|
dlg.ShowNewFolderButton = true;
|
||||||
|
var result = dlg.ShowDialog();
|
||||||
|
if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
|
||||||
|
return dlg.SelectedPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ScanSource(string path, ObservableCollection<TapeCatalog.FileEntry> files)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("path");
|
||||||
|
if (!Directory.Exists(path)) throw new DirectoryNotFoundException(path);
|
||||||
|
|
||||||
|
// normalize source root for reliable relative path calculation
|
||||||
|
var rootFull = System.IO.Path.GetFullPath(path).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(file);
|
||||||
|
// compute directory relative to source root (remove the source root prefix)
|
||||||
|
var dirFull = System.IO.Path.GetFullPath(fi.DirectoryName).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||||
|
string relativeDir = string.Empty;
|
||||||
|
if (dirFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
|
||||||
|
relativeDir = dirFull.Length == rootFull.Length ? string.Empty : dirFull.Substring(rootFull.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||||
|
else
|
||||||
|
relativeDir = fi.DirectoryName;
|
||||||
|
|
||||||
|
var existing = files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(f.FileDir, relativeDir, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
var newSizeMb = FormatBytesToMb(fi.Length);
|
||||||
|
if (existing.SizeSrc != newSizeMb)
|
||||||
|
{
|
||||||
|
existing.SizeSrc = newSizeMb;
|
||||||
|
existing.ModSrc = fi.LastWriteTime.ToString();
|
||||||
|
existing.Status = string.Empty;
|
||||||
|
existing.ArcDrive = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
files.Add(new TapeCatalog.FileEntry
|
||||||
|
{
|
||||||
|
Filename = fi.Name,
|
||||||
|
FileDir = relativeDir,
|
||||||
|
SizeSrc = FormatBytesToMb(fi.Length),
|
||||||
|
ModSrc = fi.LastWriteTime.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore inaccessible files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ScanArchive(string path, ObservableCollection<TapeCatalog.FileEntry> files)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("path");
|
||||||
|
if (!Directory.Exists(path)) throw new DirectoryNotFoundException(path);
|
||||||
|
// normalize archive root for reliable relative path calculation
|
||||||
|
var rootFull = System.IO.Path.GetFullPath(path).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(file);
|
||||||
|
|
||||||
|
var dirFull = System.IO.Path.GetFullPath(fi.DirectoryName).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||||
|
string relativeDir = string.Empty;
|
||||||
|
if (dirFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
|
||||||
|
relativeDir = dirFull.Length == rootFull.Length ? string.Empty : dirFull.Substring(rootFull.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||||
|
else
|
||||||
|
relativeDir = fi.DirectoryName;
|
||||||
|
|
||||||
|
var existing = files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(f.FileDir, relativeDir, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.SizeArc = FormatBytesToMb(fi.Length);
|
||||||
|
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 TapeCatalog.FileEntry
|
||||||
|
{
|
||||||
|
Filename = fi.Name,
|
||||||
|
FileDir = relativeDir,
|
||||||
|
SizeArc = FormatBytesToMb(fi.Length),
|
||||||
|
ModArc = fi.LastWriteTime.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore inaccessible files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] GetXmlFiles(string xmlPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(xmlPath) || !Directory.Exists(xmlPath)) return new string[0];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.GetFiles(xmlPath, "*.xml", SearchOption.TopDirectoryOnly);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new string[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatBytesToMb(long bytes)
|
||||||
|
{
|
||||||
|
double mb = bytes / 1024.0 / 1024.0;
|
||||||
|
return mb.ToString("F2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace TapeCatalog.Methods
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helpers to read XML files and produce a compact summary used by the UI.
|
||||||
|
/// </summary>
|
||||||
|
public static class Xml
|
||||||
|
{
|
||||||
|
public static List<TapeCatalog.FileEntry> ParseSummary(string filePath)
|
||||||
|
{
|
||||||
|
var result = new List<TapeCatalog.FileEntry>();
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = XDocument.Load(filePath);
|
||||||
|
var root = doc.Root;
|
||||||
|
if (root == null) return result;
|
||||||
|
|
||||||
|
if (string.Equals(root.Name.LocalName, "Files", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
foreach (var fe in root.Elements("File"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fn = fe.Element("Filename")?.Value ?? string.Empty;
|
||||||
|
var fd = fe.Element("FileDir")?.Value ?? string.Empty;
|
||||||
|
var st = fe.Element("Status")?.Value ?? string.Empty;
|
||||||
|
result.Add(new TapeCatalog.FileEntry { Filename = fn, FileDir = fd, Status = st });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore malformed entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(root.Name.LocalName, "TapeEntries", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
foreach (var entry in root.Elements("Entry"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fn = entry.Element("Filename")?.Value ?? string.Empty;
|
||||||
|
var med = entry.Element("Medium")?.Value ?? string.Empty;
|
||||||
|
var mod = entry.Element("Modified")?.Value ?? string.Empty;
|
||||||
|
result.Add(new TapeCatalog.FileEntry { Filename = fn, FileDir = med, Status = mod });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore malformed entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore parse errors and return empty list
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] GetXmlFiles(string xmlDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(xmlDirectory) || !Directory.Exists(xmlDirectory))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.GetFiles(xmlDirectory, "*.xml", SearchOption.TopDirectoryOnly);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@
|
|||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
</ApplicationDefinition>
|
</ApplicationDefinition>
|
||||||
|
<Compile Include="Methods\Copy.cs" />
|
||||||
|
<Compile Include="Methods\Settings.cs" />
|
||||||
|
<Compile Include="Methods\xml.cs" />
|
||||||
<Compile Include="XML_Methods.cs" />
|
<Compile Include="XML_Methods.cs" />
|
||||||
<Page Include="MainWindow.xaml">
|
<Page Include="MainWindow.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
|||||||
Reference in new issue
Block a user