using System.Text; using System.Text.Json; using System.Diagnostics.CodeAnalysis; using MaksIT.LTO.Core; using MaksIT.LTO.Backup.Entities; using System.Net; namespace MaksIT.LTO.Backup; public class Application { private const string _descriptoFileName = "descriptor.json"; private const string _configurationFileName = "configuration.json"; private readonly string appPath = AppDomain.CurrentDomain.BaseDirectory; private readonly string _tapePath; private readonly string _descriptorFilePath; private Configuration _configuration; public Application() { _descriptorFilePath = Path.Combine(appPath, _descriptoFileName); LoadConfiguration(); _tapePath = _configuration.TapePath; } [MemberNotNull(nameof(_configuration))] public void LoadConfiguration() { var configFilePath = Path.Combine(appPath, _configurationFileName); var configuration = JsonSerializer.Deserialize(File.ReadAllText(configFilePath)); if (configuration == null) throw new InvalidOperationException("Failed to deserialize configuration."); _configuration = configuration; } public void LoadTape() { using var handler = new TapeDeviceHandler(_tapePath); LoadTape(handler); } public void LoadTape(TapeDeviceHandler handler) { handler.Prepare(TapeDeviceHandler.TAPE_LOAD); Thread.Sleep(2000); Console.WriteLine("Tape loaded."); } public void EjectTape() { using var handler = new TapeDeviceHandler(_tapePath); EjectTape(handler); } public void EjectTape(TapeDeviceHandler handler) { handler.Prepare(TapeDeviceHandler.TAPE_UNLOAD); Thread.Sleep(2000); Console.WriteLine("Tape ejected."); } public void TapeErase() { using var handler = new TapeDeviceHandler(_tapePath); LoadTape(handler); handler.SetMediaParams(LTOBlockSizes.LTO5); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); handler.Prepare(TapeDeviceHandler.TAPE_TENSION); Thread.Sleep(2000); handler.Prepare(TapeDeviceHandler.TAPE_LOCK); Thread.Sleep(2000); handler.Erase(TapeDeviceHandler.TAPE_ERASE_SHORT); Thread.Sleep(2000); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); Console.WriteLine("Tape erased."); } public void GetDeviceStatus() { using var handler = new TapeDeviceHandler(_tapePath); handler.GetStatus(); } public void PathAccessWrapper(WorkingFolder workingFolder, Action myAction) { if (workingFolder.LocalPath != null) { var localPath = workingFolder.LocalPath.Path; var path = workingFolder.LocalPath.Path; myAction(path); } else if (workingFolder.RemotePath != null) { var remotePath = workingFolder.RemotePath; if (remotePath.Protocol == "SMB") { NetworkCredential? networkCredential = default; if (remotePath.PasswordCredentials != null) { var username = remotePath.PasswordCredentials.Username; var password = remotePath.PasswordCredentials.Password; networkCredential = new NetworkCredential(username, password); } var smbPath = remotePath.Path; if (networkCredential == null) { throw new InvalidOperationException("Network credentials are required for remote paths."); } using (new NetworkConnection(smbPath, networkCredential)) { myAction(smbPath); } } } } public void CreateDescriptor(WorkingFolder workingFolder, string descriptorFilePath, uint blockSize) { PathAccessWrapper(workingFolder, (directoryPath) => { var files = Directory.GetFiles(directoryPath, "*.*", SearchOption.AllDirectories); // Define list to hold file descriptors var descriptor = new List(); uint currentTapeBlock = 0; foreach (var filePath in files) { var fileInfo = new FileInfo(filePath); var relativePath = Path.GetRelativePath(directoryPath, filePath); var numberOfBlocks = (uint)((fileInfo.Length + blockSize - 1) / blockSize); // Optional: Calculate a simple hash for file integrity (e.g., MD5) using var md5 = System.Security.Cryptography.MD5.Create(); using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); using var bufferedStream = new BufferedStream(fileStream, (int)blockSize); byte[] buffer = new byte[blockSize]; int bytesRead; while ((bytesRead = bufferedStream.Read(buffer, 0, buffer.Length)) > 0) { md5.TransformBlock(buffer, 0, bytesRead, null, 0); } md5.TransformFinalBlock(Array.Empty(), 0, 0); string fileHash = BitConverter.ToString(md5.Hash).Replace("-", "").ToLower(); descriptor.Add(new FileDescriptor { StartBlock = currentTapeBlock, // Position of the file on the tape NumberOfBlocks = numberOfBlocks, // Number of blocks used by the file FilePath = relativePath, FileSize = fileInfo.Length, CreationTime = fileInfo.CreationTime, LastModifiedTime = fileInfo.LastWriteTime, FileHash = fileHash }); currentTapeBlock += numberOfBlocks; } // Convert descriptor list to JSON and include BlockSize string descriptorJson = JsonSerializer.Serialize(new BackupDescriptor { BlockSize = blockSize, Files = descriptor }); File.WriteAllText(descriptorFilePath, descriptorJson); }); } private void ZeroFillBlocks(TapeDeviceHandler handler, int blocks, uint blockSize) { Console.WriteLine($"Writing {blocks} zero-filled blocks to tape."); Console.WriteLine($"Block Size: {blockSize}."); var writeError = 0; for (int i = 0; i < blocks; i++) { writeError = handler.WriteData(new byte[blockSize]); if (writeError != 0) return; Thread.Sleep(_configuration.WriteDelay); } } public void WriteFilesToTape(WorkingFolder workingFolder, string descriptorFilePath, uint blockSize) { PathAccessWrapper(workingFolder, (directoryPath) => { Console.WriteLine($"Writing files to tape from: {directoryPath}."); Console.WriteLine($"Block Size: {blockSize}."); using var handler = new TapeDeviceHandler(_tapePath); LoadTape(handler); handler.SetMediaParams(blockSize); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); handler.Prepare(TapeDeviceHandler.TAPE_TENSION); Thread.Sleep(2000); handler.Prepare(TapeDeviceHandler.TAPE_LOCK); Thread.Sleep(2000); handler.WaitForTapeReady(); // Read descriptor from file system string descriptorJson = File.ReadAllText(descriptorFilePath); var descriptor = JsonSerializer.Deserialize(descriptorJson); if (descriptor == null) { throw new InvalidOperationException("Failed to deserialize descriptor."); } var currentTapeBlock = (descriptorJson.Length + blockSize - 1) / blockSize; int writeError = 0; foreach (var file in descriptor.Files) { var filePath = Path.Combine(directoryPath, file.FilePath); using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); using var bufferedStream = new BufferedStream(fileStream, (int)blockSize); byte[] buffer = new byte[blockSize]; int bytesRead; for (var i = 0; i < file.NumberOfBlocks; i++) { bytesRead = bufferedStream.Read(buffer, 0, buffer.Length); if (bytesRead < buffer.Length) { // Zero-fill the remaining part of the buffer if the last block is smaller than blockSize Array.Clear(buffer, bytesRead, buffer.Length - bytesRead); } writeError = handler.WriteData(buffer); if (writeError != 0) { Console.WriteLine($"Failed to write file: {filePath}"); return; } currentTapeBlock++; Thread.Sleep(_configuration.WriteDelay); // Small delay between blocks } } // write mark to indicate end of files handler.WriteMarks(TapeDeviceHandler.TAPE_FILEMARKS, 1); Thread.Sleep(_configuration.WriteDelay); // write descriptor to tape var descriptorData = Encoding.UTF8.GetBytes(descriptorJson); var descriptorBlocks = (descriptorData.Length + blockSize - 1) / blockSize; for (int i = 0; i < descriptorBlocks; i++) { var startIndex = i * blockSize; var length = Math.Min(blockSize, descriptorData.Length - startIndex); byte[] block = new byte[blockSize]; // Initialized with zeros by default Array.Copy(descriptorData, startIndex, block, 0, length); writeError = handler.WriteData(block); if (writeError != 0) return; currentTapeBlock++; Thread.Sleep(_configuration.WriteDelay); // Small delay between blocks } // write 3 0 filled blocks to indicate end of backup ZeroFillBlocks(handler, 3, blockSize); handler.Prepare(TapeDeviceHandler.TAPE_UNLOCK); Thread.Sleep(2000); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); }); } public BackupDescriptor? FindDescriptor(uint blockSize) { Console.WriteLine("Searching for descriptor on tape..."); Console.WriteLine($"Block Size: {blockSize}."); using var handler = new TapeDeviceHandler(_tapePath); LoadTape(handler); handler.SetMediaParams(blockSize); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); handler.SetPosition(TapeDeviceHandler.TAPE_SPACE_FILEMARKS, 0, 1); Thread.Sleep(2000); handler.WaitForTapeReady(); // Read data from tape until 3 zero-filled blocks are found var buffer = new List(); byte[] data; var zeroBlocks = 0; do { data = handler.ReadData(blockSize); buffer.AddRange(data); if (data.All(b => b == 0)) { zeroBlocks++; } else { zeroBlocks = 0; } } while (zeroBlocks < 3); // Remove the last 3 zero-filled blocks from the buffer var totalZeroBlocksSize = (int)(3 * blockSize); if (buffer.Count >= totalZeroBlocksSize) { buffer.RemoveRange(buffer.Count - totalZeroBlocksSize, totalZeroBlocksSize); } // Convert buffer to byte array var byteArray = buffer.ToArray(); // Convert byte array to string and trim ending zeros var json = Encoding.UTF8.GetString(byteArray).TrimEnd('\0'); try { var descriptor = JsonSerializer.Deserialize(json); if (descriptor != null) { Console.WriteLine("Descriptor read successfully."); return descriptor; } } catch (JsonException ex) { Console.WriteLine($"Failed to parse descriptor JSON: {ex.Message}"); } handler.Prepare(TapeDeviceHandler.TAPE_UNLOCK); Thread.Sleep(2000); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); return null; } public void RestoreDirectory(BackupDescriptor descriptor, WorkingFolder workingFolder) { PathAccessWrapper(workingFolder, (restoreDirectoryPath) => { Console.WriteLine("Restoring files to directory: " + restoreDirectoryPath); Console.WriteLine("Block Size: " + descriptor.BlockSize); using var handler = new TapeDeviceHandler(_tapePath); LoadTape(handler); handler.SetMediaParams(descriptor.BlockSize); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); foreach (var file in descriptor.Files) { // Set position to the start block of the file handler.SetPosition(TapeDeviceHandler.TAPE_ABSOLUTE_BLOCK, 0, file.StartBlock); Thread.Sleep(2000); var filePath = Path.Combine(restoreDirectoryPath, file.FilePath); var directoryPath = Path.GetDirectoryName(filePath); if (directoryPath != null) { Directory.CreateDirectory(directoryPath); } using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { var buffer = new byte[descriptor.BlockSize]; for (var i = 0; i < file.NumberOfBlocks; i++) { var bytesRead = handler.ReadData(buffer, 0, buffer.Length); if (bytesRead < buffer.Length) { // Zero-fill the remaining part of the buffer if the last block is smaller than blockSize Array.Clear(buffer, bytesRead, buffer.Length - bytesRead); } var bytesToWrite = (i == file.NumberOfBlocks - 1) ? (int)(file.FileSize % descriptor.BlockSize) : buffer.Length; fileStream.Write(buffer, 0, bytesToWrite); } } // check md5 checksum of restored file with the one in descriptor using (var md5 = System.Security.Cryptography.MD5.Create()) { using (var fileStreamRead = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { var fileHash = md5.ComputeHash(fileStreamRead); var fileHashString = BitConverter.ToString(fileHash).Replace("-", "").ToLower(); if (fileHashString != file.FileHash) { Console.WriteLine($"Checksum mismatch for file: {filePath}"); } else { Console.WriteLine($"Restored file: {filePath}"); } } } } handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); Thread.Sleep(2000); }); } public int CheckMediaSize(string ltoGen) { var descriptor = JsonSerializer.Deserialize(File.ReadAllText(_descriptorFilePath)); if (descriptor == null) { Console.WriteLine("Failed to read descriptor."); return 1; } var totalBlocks = (ulong)descriptor.Files.Sum(f => f.NumberOfBlocks); const ulong fileMarkBlocks = 1; const ulong terminalBlocks = 3; var descriptorSize = new FileInfo(_descriptoFileName).Length; ulong descriptorSizeBlocks = (ulong)Math.Ceiling((double)descriptorSize / descriptor.BlockSize); totalBlocks += fileMarkBlocks + descriptorSizeBlocks + terminalBlocks; var maxBlocks = LTOBlockSizes.GetMaxBlocks(ltoGen); if (totalBlocks > maxBlocks) { Console.WriteLine("Backup will not fit on tape. Please use a larger tape."); return 1; } else { Console.WriteLine("Backup will fit on tape."); } return 0; } public void Backup() { while (true) { Console.WriteLine("\nSelect a backup to perform:"); for (int i = 0; i < _configuration.Backups.Count; i++) { var backupInt = _configuration.Backups[i]; Console.WriteLine($"{i + 1}. Backup Name: {backupInt.Name}, Bar code {(string.IsNullOrEmpty(backupInt.Barcode) ? "None" : backupInt.Barcode)}, Source: {backupInt.Source}, Destination: {backupInt.Destination}"); } Console.Write("Enter your choice (or '0' to go back): "); var choice = Console.ReadLine(); if (choice == "0") { return; // Go back to the main menu } if (!int.TryParse(choice, out int index) || index < 1 || index > _configuration.Backups.Count) { Console.WriteLine("Invalid choice. Please try again."); continue; } var backup = _configuration.Backups[index - 1]; uint blockSize = LTOBlockSizes.GetBlockSize(backup.LTOGen); // Step 1: Create Descriptor and Write to File System CreateDescriptor(backup.Source, _descriptorFilePath, blockSize); // Step 2: calculate if files in descriptor will fit on tape var checkMediaSizeResult = CheckMediaSize(backup.LTOGen); if (checkMediaSizeResult != 0) return; // Step 3: Write Files to Tape WriteFilesToTape(backup.Source, _descriptorFilePath, blockSize); File.Delete(_descriptorFilePath); Console.WriteLine("Backup completed."); return; // Go back to the main menu after completing the backup } } public void Restore() { while (true) { Console.WriteLine("\nSelect a backup to restore:"); for (int i = 0; i < _configuration.Backups.Count; i++) { var backupInt = _configuration.Backups[i]; Console.WriteLine($"{i + 1}. Backup Name: {backupInt.Name}, Bar code {(string.IsNullOrEmpty(backupInt.Barcode) ? "None" : backupInt.Barcode)}, Source: {backupInt.Source}, Destination: {backupInt.Destination}"); } Console.Write("Enter your choice (or '0' to go back): "); var choice = Console.ReadLine(); if (choice == "0") { return; // Go back to the main menu } if (!int.TryParse(choice, out int index) || index < 1 || index > _configuration.Backups.Count) { Console.WriteLine("Invalid choice. Please try again."); continue; } var backup = _configuration.Backups[index - 1]; uint blockSize = LTOBlockSizes.GetBlockSize(backup.LTOGen); var descriptor = FindDescriptor(blockSize); if (descriptor != null) { var json = JsonSerializer.Serialize(descriptor, new JsonSerializerOptions { WriteIndented = true }); Console.WriteLine(json); } if (descriptor == null) { Console.WriteLine("Descriptor not found on tape."); return; } // Step 3: Test restore from tape RestoreDirectory(descriptor, backup.Destination); Console.WriteLine("Restore completed."); return; // Go back to the main menu after completing the restore } } }