From 4826d124f7d1f14951e1276fb700f1d10d30e407 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 1 Nov 2024 04:14:01 -0700 Subject: [PATCH 1/4] (feature): get status and fine tuning improvements --- src/MaksIT.LTO.Backup/Application.cs | 16 +++++++++++----- src/MaksIT.LTO.Backup/Configuration.cs | 1 + src/MaksIT.LTO.Backup/Program.cs | 11 +++++++---- src/MaksIT.LTO.Backup/configuration.json | 15 ++++++++------- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/MaksIT.LTO.Backup/Application.cs b/src/MaksIT.LTO.Backup/Application.cs index f6738c8..2049dfc 100644 --- a/src/MaksIT.LTO.Backup/Application.cs +++ b/src/MaksIT.LTO.Backup/Application.cs @@ -59,6 +59,11 @@ public class Application { Console.WriteLine("Tape ejected."); } + public void GetDeviceStatus() { + using var handler = new TapeDeviceHandler(_tapePath); + handler.GetStatus(); + } + public void PathAccessWrapper(WorkingFolder workingFolder, Action myAction) { @@ -150,7 +155,7 @@ public class Application { for (int i = 0; i < blocks; i++) { handler.WriteData(new byte[blockSize]); - Thread.Sleep(100); + Thread.Sleep(_configuration.WriteDelay); } } @@ -201,13 +206,14 @@ public class Application { } handler.WriteData(buffer); currentTapeBlock++; - Thread.Sleep(100); // Small delay between blocks + 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); @@ -219,7 +225,7 @@ public class Application { Array.Copy(descriptorData, startIndex, block, 0, length); handler.WriteData(block); currentTapeBlock++; - Thread.Sleep(100); // Small delay between blocks + Thread.Sleep(_configuration.WriteDelay); // Small delay between blocks } // write 3 0 filled blocks to indicate end of backup @@ -396,7 +402,7 @@ public class Application { 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 {backupInt.Barcode}, Source: {backupInt.Source}, Destination: {backupInt.Destination}"); + 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): "); @@ -437,7 +443,7 @@ public class Application { 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 {backupInt.Barcode}, Source: {backupInt.Source}, Destination: {backupInt.Destination}"); + 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): "); diff --git a/src/MaksIT.LTO.Backup/Configuration.cs b/src/MaksIT.LTO.Backup/Configuration.cs index e29bcd8..afecb5b 100644 --- a/src/MaksIT.LTO.Backup/Configuration.cs +++ b/src/MaksIT.LTO.Backup/Configuration.cs @@ -35,5 +35,6 @@ public class BackupItem { public class Configuration { public required string TapePath { get; set; } + public required int WriteDelay { get; set; } public required List Backups { get; set; } } diff --git a/src/MaksIT.LTO.Backup/Program.cs b/src/MaksIT.LTO.Backup/Program.cs index 52f9cc5..85fceda 100644 --- a/src/MaksIT.LTO.Backup/Program.cs +++ b/src/MaksIT.LTO.Backup/Program.cs @@ -18,8 +18,9 @@ class Program { Console.WriteLine("2. Backup"); Console.WriteLine("3. Restore"); Console.WriteLine("4. Eject tape"); - Console.WriteLine("5. Reload configurations"); - Console.WriteLine("6. Exit"); + Console.WriteLine("5. Get device status"); + Console.WriteLine("6. Reload configurations"); + Console.WriteLine("7. Exit"); Console.Write("Enter your choice: "); var choice = Console.ReadLine(); @@ -38,11 +39,13 @@ class Program { case "4": app.EjectTape(); break; - case "5": - app.LoadConfiguration(); + app.GetDeviceStatus(); break; case "6": + app.LoadConfiguration(); + break; + case "7": Console.WriteLine("Exiting..."); return; default: diff --git a/src/MaksIT.LTO.Backup/configuration.json b/src/MaksIT.LTO.Backup/configuration.json index b320264..c0fff25 100644 --- a/src/MaksIT.LTO.Backup/configuration.json +++ b/src/MaksIT.LTO.Backup/configuration.json @@ -1,5 +1,6 @@ { "TapePath": "\\\\.\\Tape0", + "WriteDelay": 100, "Backups": [ { "Name": "Normal test", @@ -7,7 +8,7 @@ "LTOGen": "LTO5", "Source": { "LocalPath": { - "Path": "D:\\Drivers" + "Path": "F:\\LTO\\Backup" } }, "Destination": { @@ -22,17 +23,17 @@ "LTOGen": "LTO5", "Source": { "RemotePath": { - "Path": "\\\\nasrv0001.corp.maks-it.com\\LTO\\Backup", + "Path": "\\\\nasrv0002.corp.maks-it.com\\Users", "Credentials": { - "Username": "user", - "Password": "password" - } + "Username": "maksym", + "Password": "05Train0888" + }, + "Protocol": "SMB" } }, "Destination": { - "Path": "F:\\LTO\\Restore" + "Path": "F:\\LTO\\Restore\\Users" } } -} ] } \ No newline at end of file From b518360409976c902cebe805ebdb7113018fdac7 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 1 Nov 2024 04:36:43 -0700 Subject: [PATCH 2/4] (feature): improved write error handling --- src/MaksIT.LTO.Backup/Application.cs | 22 ++++++++++++-- src/MaksIT.LTO.Core/DriverManager.cs | 37 ++++++++++++++++++++++++ src/MaksIT.LTO.Core/TapeDeviceHandler.cs | 5 +++- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/MaksIT.LTO.Core/DriverManager.cs diff --git a/src/MaksIT.LTO.Backup/Application.cs b/src/MaksIT.LTO.Backup/Application.cs index 59d7609..8d8188f 100644 --- a/src/MaksIT.LTO.Backup/Application.cs +++ b/src/MaksIT.LTO.Backup/Application.cs @@ -153,8 +153,13 @@ public class Application { Console.WriteLine($"Writing {blocks} zero-filled blocks to tape."); Console.WriteLine($"Block Size: {blockSize}."); + var writeError = 0; + for (int i = 0; i < blocks; i++) { - handler.WriteData(new byte[blockSize]); + writeError = handler.WriteData(new byte[blockSize]); + if (writeError != 0) + return; + Thread.Sleep(_configuration.WriteDelay); } } @@ -191,6 +196,9 @@ public class Application { 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); @@ -204,7 +212,11 @@ public class Application { // Zero-fill the remaining part of the buffer if the last block is smaller than blockSize Array.Clear(buffer, bytesRead, buffer.Length - bytesRead); } - handler.WriteData(buffer); + + writeError = handler.WriteData(buffer); + if (writeError != 0) + return; + currentTapeBlock++; Thread.Sleep(_configuration.WriteDelay); // Small delay between blocks } @@ -223,7 +235,11 @@ public class Application { 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); - handler.WriteData(block); + + writeError = handler.WriteData(block); + if (writeError != 0) + return; + currentTapeBlock++; Thread.Sleep(_configuration.WriteDelay); // Small delay between blocks } diff --git a/src/MaksIT.LTO.Core/DriverManager.cs b/src/MaksIT.LTO.Core/DriverManager.cs new file mode 100644 index 0000000..77395a5 --- /dev/null +++ b/src/MaksIT.LTO.Core/DriverManager.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.LTO.Core; +public class DriverManager { + public static void RestartDriver(string deviceName) { + string script = $@" + $device = Get-PnpDevice -FriendlyName '{deviceName}' + Disable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false + Start-Sleep -Seconds 5 + Enable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false + "; + + ProcessStartInfo psi = new ProcessStartInfo { + FileName = "powershell.exe", + Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{script}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using (Process process = Process.Start(psi)) { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + Console.WriteLine(output); + if (!string.IsNullOrEmpty(error)) { + Console.WriteLine($"Error: {error}"); + } + } + } +} diff --git a/src/MaksIT.LTO.Core/TapeDeviceHandler.cs b/src/MaksIT.LTO.Core/TapeDeviceHandler.cs index ad0ef0a..dc4645b 100644 --- a/src/MaksIT.LTO.Core/TapeDeviceHandler.cs +++ b/src/MaksIT.LTO.Core/TapeDeviceHandler.cs @@ -82,7 +82,7 @@ public partial class TapeDeviceHandler : IDisposable { } - public void WriteData(byte[] data) { + public int WriteData(byte[] data) { IntPtr unmanagedPointer = Marshal.AllocHGlobal(data.Length); try { Marshal.Copy(data, 0, unmanagedPointer, data.Length); @@ -94,11 +94,14 @@ public partial class TapeDeviceHandler : IDisposable { else { int error = Marshal.GetLastWin32Error(); Console.WriteLine($"Write Data: Failed with error code {error}"); + return error; } } finally { Marshal.FreeHGlobal(unmanagedPointer); } + + return 0; } public byte[] ReadData(uint length) { From 95bf055a1c9f244a6425baa716128a1f06710a88 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 1 Nov 2024 08:31:43 -0700 Subject: [PATCH 3/4] (feature): local backup and restore tested --- src/MaksIT.LTO.Backup/Application.cs | 61 +++++++++++++------ src/MaksIT.LTO.Backup/Program.cs | 10 ++- src/MaksIT.LTO.Backup/configuration.json | 4 +- .../TapeDeviceHandlerNtdtape.cs | 5 +- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/MaksIT.LTO.Backup/Application.cs b/src/MaksIT.LTO.Backup/Application.cs index 8d8188f..d7ab5f5 100644 --- a/src/MaksIT.LTO.Backup/Application.cs +++ b/src/MaksIT.LTO.Backup/Application.cs @@ -42,7 +42,7 @@ public class Application { public void LoadTape(TapeDeviceHandler handler) { handler.Prepare(TapeDeviceHandler.TAPE_LOAD); - handler.WaitForTapeReady(); + Thread.Sleep(2000); Console.WriteLine("Tape loaded."); } @@ -54,11 +54,36 @@ public class Application { public void EjectTape(TapeDeviceHandler handler) { handler.Prepare(TapeDeviceHandler.TAPE_UNLOAD); - handler.WaitForTapeReady(); + 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(); @@ -176,13 +201,13 @@ public class Application { handler.SetMediaParams(blockSize); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); - handler.WaitForTapeReady(); + Thread.Sleep(2000); handler.Prepare(TapeDeviceHandler.TAPE_TENSION); - handler.WaitForTapeReady(); + Thread.Sleep(2000); handler.Prepare(TapeDeviceHandler.TAPE_LOCK); - handler.WaitForTapeReady(); + Thread.Sleep(2000); handler.WaitForTapeReady(); @@ -214,8 +239,10 @@ public class Application { } writeError = handler.WriteData(buffer); - if (writeError != 0) + if (writeError != 0) { + Console.WriteLine($"Failed to write file: {filePath}"); return; + } currentTapeBlock++; Thread.Sleep(_configuration.WriteDelay); // Small delay between blocks @@ -248,9 +275,10 @@ public class Application { ZeroFillBlocks(handler, 3, blockSize); handler.Prepare(TapeDeviceHandler.TAPE_UNLOCK); - handler.WaitForTapeReady(); + Thread.Sleep(2000); + handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); - handler.WaitForTapeReady(); + Thread.Sleep(2000); }); } @@ -266,10 +294,10 @@ public class Application { handler.SetMediaParams(blockSize); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); - handler.WaitForTapeReady(); + Thread.Sleep(2000); handler.SetPosition(TapeDeviceHandler.TAPE_SPACE_FILEMARKS, 0, 1); - handler.WaitForTapeReady(); + Thread.Sleep(2000); handler.WaitForTapeReady(); @@ -313,9 +341,10 @@ public class Application { handler.Prepare(TapeDeviceHandler.TAPE_UNLOCK); - handler.WaitForTapeReady(); + Thread.Sleep(2000); + handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); - handler.WaitForTapeReady(); + Thread.Sleep(2000); return null; } @@ -333,14 +362,12 @@ public class Application { handler.SetMediaParams(descriptor.BlockSize); handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); - handler.WaitForTapeReady(); - - handler.WaitForTapeReady(); + 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); - handler.WaitForTapeReady(); + Thread.Sleep(2000); var filePath = Path.Combine(restoreDirectoryPath, file.FilePath); var directoryPath = Path.GetDirectoryName(filePath); @@ -380,7 +407,7 @@ public class Application { } handler.SetPosition(TapeDeviceHandler.TAPE_REWIND); - handler.WaitForTapeReady(); + Thread.Sleep(2000); }); } diff --git a/src/MaksIT.LTO.Backup/Program.cs b/src/MaksIT.LTO.Backup/Program.cs index 85fceda..64c7852 100644 --- a/src/MaksIT.LTO.Backup/Program.cs +++ b/src/MaksIT.LTO.Backup/Program.cs @@ -19,8 +19,9 @@ class Program { Console.WriteLine("3. Restore"); Console.WriteLine("4. Eject tape"); Console.WriteLine("5. Get device status"); - Console.WriteLine("6. Reload configurations"); - Console.WriteLine("7. Exit"); + Console.WriteLine("6. Tape Erase (Short)"); + Console.WriteLine("7. Reload configurations"); + Console.WriteLine("8. Exit"); Console.Write("Enter your choice: "); var choice = Console.ReadLine(); @@ -43,9 +44,12 @@ class Program { app.GetDeviceStatus(); break; case "6": - app.LoadConfiguration(); + app.TapeErase(); break; case "7": + app.LoadConfiguration(); + break; + case "8": Console.WriteLine("Exiting..."); return; default: diff --git a/src/MaksIT.LTO.Backup/configuration.json b/src/MaksIT.LTO.Backup/configuration.json index 68dfb0d..45f8825 100644 --- a/src/MaksIT.LTO.Backup/configuration.json +++ b/src/MaksIT.LTO.Backup/configuration.json @@ -1,6 +1,6 @@ { "TapePath": "\\\\.\\Tape0", - "WriteDelay": 100, + "WriteDelay": 1000, "Backups": [ { "Name": "Normal test", @@ -8,7 +8,7 @@ "LTOGen": "LTO5", "Source": { "LocalPath": { - "Path": "F:\\LTO\\Backup" + "Path": "D:\\Program Files" } }, "Destination": { diff --git a/src/MaksIT.LTO.Core/TapeDeviceHandlerNtdtape.cs b/src/MaksIT.LTO.Core/TapeDeviceHandlerNtdtape.cs index 66a312d..62e847d 100644 --- a/src/MaksIT.LTO.Core/TapeDeviceHandlerNtdtape.cs +++ b/src/MaksIT.LTO.Core/TapeDeviceHandlerNtdtape.cs @@ -325,7 +325,7 @@ public partial class TapeDeviceHandler : IDisposable { /// Erase the tape /// /// The type of erase operation. Valid values are and . - public void Erase(uint type) { + public int Erase(uint type) { TAPE_ERASE erase = new TAPE_ERASE { Type = type, Immediate = 0 @@ -343,11 +343,14 @@ public partial class TapeDeviceHandler : IDisposable { else { int error = Marshal.GetLastWin32Error(); Console.WriteLine($"Erase Tape: Failed with error code {error}"); + return error; } } finally { Marshal.FreeHGlobal(inBuffer); } + + return 0; } /// From ba0d08a89edb6e731bd9f81e61be38ec7e4117f2 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 1 Nov 2024 10:14:27 -0700 Subject: [PATCH 4/4] (feature): Backup from SMB tested --- README.md | 102 ++++++++++++++++++----- src/MaksIT.LTO.Backup/Configuration.cs | 2 +- src/MaksIT.LTO.Backup/configuration.json | 12 +-- 3 files changed, 90 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 776a030..3c325c6 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,52 @@ A C# application designed to facilitate backup and restore operations to an LTO 1. Clone the repository: ```bash - git clone https://github.com/your-username/MaksIT.LTO.Backup.git + git clone https://github.com/MAKS-IT-COM/maksit-lto-backup ``` -2. Ensure `.NET Core` is installed on your system. +2. Ensure `.NET8 SDK` is installed on your system. 3. Prepare a `configuration.json` file in the application directory with the following structure: ```json - { - "TapePath": "YourTapePath", - "Backups": [ - { - "Name": "BackupName", - "Barcode": "123456", - "Source": "path/to/source", - "Destination": "path/to/destination", - "LTOGen": "LTO6" - } - ] - } + { + "TapePath": "\\\\.\\Tape0", + "WriteDelay": 100, + "Backups": [ + { + "Name": "Normal test", + "Barcode": "", + "LTOGen": "LTO5", + "Source": { + "LocalPath": { + "Path": "F:\\LTO\\Backup" + } + }, + "Destination": { + "LocalPath": { + "Path": "F:\\LTO\\Restore" + } + } + }, + { + "Name": "Network test", + "Barcode": "", + "LTOGen": "LTO5", + "Source": { + "RemotePath": { + "Path": "\\\\nassrv0001.corp.maks-it.com\\data-1\\Users", + "PasswordCredentials": { + "Username": "", + "Password": "" + }, + "Protocol": "SMB" + } + }, + "Destination": { + "LocalPath": { + "Path": "F:\\LTO\\Restore" + } + } + } + ] + } ``` ## Usage @@ -57,7 +86,11 @@ Upon running, the following options will be presented: 2. **Backup**: Prompts the user to select a backup task from the configured list and initiates the backup process. 3. **Restore**: Restores a previously backed-up directory from the tape. 4. **Eject Tape**: Ejects the tape from the drive safely. -5. **Exit**: Exits the application. +5. **Get device status**: Mostly used for debugging to understand if device is able to write +6. **Tape Erase (Short)**: +7. **Reload configurations** +6. **Exit**: Exits the application. + ### Code Overview @@ -72,13 +105,42 @@ Below is an example configuration setup for an LTO-6 tape generation backup oper ```json { "TapePath": "\\\\.\\Tape0", + "WriteDelay": 100, "Backups": [ { - "Name": "Monthly Backup", - "Barcode": "MB12345", - "Source": "/path/to/source", - "Destination": "/path/to/restore", - "LTOGen": "LTO6" + "Name": "Normal test", + "Barcode": "", + "LTOGen": "LTO5", + "Source": { + "LocalPath": { + "Path": "F:\\LTO\\Backup" + } + }, + "Destination": { + "LocalPath": { + "Path": "F:\\LTO\\Restore" + } + } + }, + { + "Name": "Network test", + "Barcode": "", + "LTOGen": "LTO5", + "Source": { + "RemotePath": { + "Path": "\\\\nassrv0001.corp.maks-it.com\\data-1\\Users", + "PasswordCredentials": { + "Username": "", + "Password": "" + }, + "Protocol": "SMB" + } + }, + "Destination": { + "LocalPath": { + "Path": "F:\\LTO\\Restore\\Users" + } + } } ] } diff --git a/src/MaksIT.LTO.Backup/Configuration.cs b/src/MaksIT.LTO.Backup/Configuration.cs index afecb5b..4ec91fe 100644 --- a/src/MaksIT.LTO.Backup/Configuration.cs +++ b/src/MaksIT.LTO.Backup/Configuration.cs @@ -12,7 +12,7 @@ public class LocalPath : PathBase { public class PasswordCredentials { public required string Username { get; set; } - public required SecureString Password { get; set; } + public required string Password { get; set; } } public class RemotePath : PathBase { diff --git a/src/MaksIT.LTO.Backup/configuration.json b/src/MaksIT.LTO.Backup/configuration.json index 45f8825..f41b3b3 100644 --- a/src/MaksIT.LTO.Backup/configuration.json +++ b/src/MaksIT.LTO.Backup/configuration.json @@ -1,6 +1,6 @@ { "TapePath": "\\\\.\\Tape0", - "WriteDelay": 1000, + "WriteDelay": 100, "Backups": [ { "Name": "Normal test", @@ -8,7 +8,7 @@ "LTOGen": "LTO5", "Source": { "LocalPath": { - "Path": "D:\\Program Files" + "Path": "F:\\LTO\\Backup" } }, "Destination": { @@ -23,8 +23,8 @@ "LTOGen": "LTO5", "Source": { "RemotePath": { - "Path": "\\\\nasrv0002.corp.maks-it.com\\Users", - "Credentials": { + "Path": "\\\\nassrv0002.corp.maks-it.com\\data-1\\Users", + "PasswordCredentials": { "Username": "", "Password": "" }, @@ -32,7 +32,9 @@ } }, "Destination": { - "Path": "F:\\LTO\\Restore\\Users" + "LocalPath": { + "Path": "F:\\LTO\\Restore" + } } } ]