1- // Copyright 2026 Kyle Ebbinga
1+ // Copyright 2026 Entex Interactive
22
33using System . Collections . Concurrent ;
44using System . CommandLine ;
@@ -19,27 +19,45 @@ public class RestoreCommand : Command
1919 {
2020 private Stopwatch _sw = new Stopwatch ( ) ;
2121
22- private readonly Option < string > _sourceOpt = new ( [ "--path" , "-p" ] , "The source path to restore." ) ;
22+ private readonly Argument < string > _sourceArg = new ( "path" , "The path to add or remove." ) ;
23+ private readonly Option < string > _sourceOpt = new ( [ "--path" , "-p" ] , "The source path to pull." ) ;
2324 private readonly Option < string > _configOpt = new ( [ "--config" , "-c" ] , "The vault configuration to use." ) ;
24- private readonly Option < DateTime > _beforeOpt = new ( [ "--before" ] , "Restores files before a certain timestamp." ) ;
25- private readonly Option < string > _remapOpt = new ( [ "--remap" ] , "The new directory to map restored files to." ) ;
26- private readonly Option < bool > _forceOpt = new ( [ "--force" , "-f" ] , "Forces restoring, bypassing safe guards." ) ;
25+ private readonly Option < DateTime > _beforeOpt = new ( [ "--before" ] , "Pulls files before a certain timestamp." ) ;
26+ private readonly Option < string > _destOpt = new ( [ "--destination" ] , "The new directory to map pulled files to." ) ;
27+ private readonly Option < bool > _archiveOpt = new ( [ "--archive" , "-a" ] , "Pulls only archived files." ) ;
28+ private readonly Option < bool > _forceOpt = new ( [ "--force" , "-f" ] , "Forces pulling, bypassing safe guards." ) ;
2729 private readonly Option < bool > _dryRunOpt = new ( [ "--dry-run" ] , "Previews the command without executing it." ) ;
2830 private readonly Option < bool > _verboseOpt = new ( [ "--verbose" , "-v" ] , "Shows verbose output." ) ;
31+
32+ private readonly Command addCmd = new ( "add" , "Adds a new directory to the backup list." ) ;
33+ private readonly Command listCmd = new ( "list" , "Shows all directories in the backup list." ) ;
34+ private readonly Command removeCmd = new ( "remove" , "Removes a directory from the backup list." ) ;
2935
30- public RestoreCommand ( ) : base ( "restore" , "Restores files from the backup ." )
36+ public RestoreCommand ( ) : base ( "restore" , "Pulls files a vault ." )
3137 {
3238 this . AddOption ( _sourceOpt ) ;
3339 this . AddOption ( _configOpt ) ;
3440 this . AddOption ( _beforeOpt ) ;
35- this . AddOption ( _remapOpt ) ;
41+ this . AddOption ( _destOpt ) ;
42+ this . AddOption ( _archiveOpt ) ;
3643 this . AddOption ( _forceOpt ) ;
3744 this . AddOption ( _dryRunOpt ) ;
3845 this . AddOption ( _verboseOpt ) ;
39- this . SetHandler ( HandleRestoreAsync , _sourceOpt , _configOpt , _beforeOpt , _remapOpt , _forceOpt , _verboseOpt , _dryRunOpt ) ;
46+ this . SetHandler ( HandlePullAsync , _sourceOpt , _configOpt , _beforeOpt , _destOpt , _archiveOpt , _forceOpt , _verboseOpt , _dryRunOpt ) ;
47+
48+ this . AddCommand ( addCmd ) ;
49+ addCmd . AddArgument ( _sourceArg ) ;
50+ addCmd . AddOption ( _configOpt ) ;
51+ addCmd . AddOption ( _destOpt ) ;
52+ addCmd . SetHandler ( HandleAddAsync , _sourceArg , _configOpt , _destOpt ) ;
53+
54+ this . AddCommand ( removeCmd ) ;
55+ removeCmd . AddArgument ( _sourceArg ) ;
56+ removeCmd . AddOption ( _configOpt ) ;
57+ removeCmd . SetHandler ( HandleRemoveAsync , _sourceArg , _configOpt ) ;
4058 }
4159
42- private async Task HandleRestoreAsync ( string ? path , string ? config , DateTime before , string ? remap , bool force , bool verbose , bool dryRun )
60+ private async Task HandlePullAsync ( string ? path , string ? config , DateTime before , string ? destination , bool archive , bool force , bool verbose , bool dryRun )
4361 {
4462 _sw = Stopwatch . StartNew ( ) ;
4563 DateTime timestamp = before != DateTime . MinValue ? before . AddMinutes ( 1 ) . AddTicks ( - 1 ) : DateTime . Now ;
@@ -48,27 +66,27 @@ private async Task HandleRestoreAsync(string? path, string? config, DateTime bef
4866 {
4967 if ( ! string . IsNullOrEmpty ( path ) )
5068 {
51- await RestorePathAsync ( localVault , path , timestamp , remap , force , verbose , dryRun ) ;
69+ await PullPathAsync ( localVault , path , timestamp , destination , archive , force , verbose , dryRun ) ;
5270 }
5371 else
5472 {
55- await RestoreSystemAsync ( localVault , timestamp , remap , force , verbose , dryRun ) ;
73+ await PullSystemAsync ( localVault , timestamp , destination , archive , force , verbose , dryRun ) ;
5674 }
5775 }
5876 else
5977 {
6078 if ( ! string . IsNullOrEmpty ( path ) )
6179 {
62- await Program . Settings . ForEachVaultAsync ( vault => RestorePathAsync ( vault , path , timestamp , remap , force , verbose , dryRun ) ) ;
80+ await Program . Settings . ForEachVaultAsync ( vault => PullPathAsync ( vault , path , timestamp , destination , archive , force , verbose , dryRun ) ) ;
6381 }
6482 else
6583 {
66- await Program . Settings . ForEachVaultAsync ( vault => RestoreSystemAsync ( vault , timestamp , remap , force , verbose , dryRun ) ) ;
84+ await Program . Settings . ForEachVaultAsync ( vault => PullSystemAsync ( vault , timestamp , destination , archive , force , verbose , dryRun ) ) ;
6785 }
6886 }
6987 }
7088
71- private async Task RestoreSystemAsync ( LocalVaultConfig vault , DateTime timestamp , string ? output , bool force , bool verbose , bool dryRun )
89+ private async Task PullSystemAsync ( LocalVaultConfig vault , DateTime timestamp , string ? output , bool archive , bool force , bool verbose , bool dryRun )
7290 {
7391 ISyncManager ? syncManager = SyncManager . CreateNew ( vault ) ;
7492 if ( syncManager == null || ! await syncManager . ConnectAsync ( ) )
@@ -77,13 +95,13 @@ private async Task RestoreSystemAsync(LocalVaultConfig vault, DateTime timestamp
7795 return ;
7896 }
7997
80- foreach ( string path in syncManager . RemoteVault . BackupDirectories )
98+ foreach ( PullRecord record in syncManager . RemoteVault . PullDirectories . Where ( r => r . Machine == Environment . MachineName ) )
8199 {
82- await RestoreInternalAsync ( syncManager , path , timestamp , output , force , verbose , dryRun ) ;
100+ await PullInternalAsync ( syncManager , record . Source , timestamp , ( output ?? record . Destination ) , archive , force , verbose , dryRun ) ;
83101 }
84102 }
85103
86- private async Task RestorePathAsync ( LocalVaultConfig vault , string path , DateTime timestamp , string ? output , bool force , bool verbose , bool dryRun )
104+ private async Task PullPathAsync ( LocalVaultConfig vault , string path , DateTime timestamp , string ? destination , bool archive , bool force , bool verbose , bool dryRun )
87105 {
88106 ISyncManager ? syncManager = SyncManager . CreateNew ( vault ) ;
89107 if ( syncManager == null || ! await syncManager . ConnectAsync ( ) )
@@ -92,23 +110,23 @@ private async Task RestorePathAsync(LocalVaultConfig vault, string path, DateTim
92110 return ;
93111 }
94112
95- await RestoreInternalAsync ( syncManager , path , timestamp , output , force , verbose , dryRun ) ;
113+ await PullInternalAsync ( syncManager , path , timestamp , destination , archive , force , verbose , dryRun ) ;
96114 }
97115
98- private async Task RestoreInternalAsync ( ISyncManager syncManager , string path , DateTime timestamp , string ? output , bool force , bool verbose , bool dryRun )
116+ private async Task PullInternalAsync ( ISyncManager syncManager , string path , DateTime timestamp , string ? destination , bool archive , bool force , bool verbose , bool dryRun )
99117 {
100118 CommandLine . WriteLine ( syncManager . RemoteVault , $ "Scanning for files in { path } ...", ConsoleColor . DarkGray ) ;
101- IReadOnlyList < LocalFile > files = await ( syncManager . Database ? . GetLatestFilesAsync ( path , timestamp ) ?? Task . FromResult < IReadOnlyList < LocalFile > > ( [ ] ) ) ;
119+ IReadOnlyList < LocalFile > files = await ( syncManager . Database ? . GetLatestFilesAsync ( path , timestamp , archive ) ?? Task . FromResult < IReadOnlyList < LocalFile > > ( [ ] ) ) ;
102120 Log . Debug ( $ "GetLatestFilesAsync returned { files . Count } files for path '{ path } '") ;
103121
104122 ConcurrentBag < LocalFile > restoreFiles = new ( ) ;
105123 System . Threading . Tasks . Parallel . ForEach ( files , ParallelConfig . Options , ( file ) =>
106124 {
107125 //string sourcePath = file.Fullname;
108126 //string outputPath = string.IsNullOrEmpty(output) ? sourcePath : PathBuilder.ReplacePath(sourcePath, path, output);
109- string outputPath = PathBuilder . ReplacePath ( file . Fullname , path , output ) ;
127+ string outputPath = PathBuilder . ReplacePath ( file . Fullname , path , destination ) ;
110128 if ( File . Exists ( outputPath ) && ! FileScanner . HasChanged ( file , new LocalFile ( outputPath ) ) && ! force ) return ;
111-
129+
112130 file . Fullname = outputPath ;
113131 restoreFiles . Add ( file ) ;
114132 } ) ;
@@ -123,18 +141,92 @@ private async Task RestoreInternalAsync(ISyncManager syncManager, string path, D
123141 {
124142 string fileName = PathBuilder . TempFile ;
125143 await File . WriteAllLinesAsync ( fileName , restoreFiles . Select ( f => f . Fullname ) . OrderBy ( f => f ) ) ;
126- CommandLine . WriteLine ( $ "This operation will restore { restoreFiles . Count : N0} files into: { ( string . IsNullOrEmpty ( output ) ? path : output ) } ", ConsoleColor . Green ) ;
144+ CommandLine . WriteLine ( $ "This operation will pull { restoreFiles . Count : N0} files into: { ( string . IsNullOrEmpty ( destination ) ? path : destination ) } ", ConsoleColor . Green ) ;
127145 CommandLine . WriteLine ( $ "A detailed list can be found here: { fileName } ", ConsoleColor . DarkGray ) ;
128146 }
129147 else
130148 {
131- CommandLine . WriteLine ( syncManager . RemoteVault , $ "Restoring { restoreFiles . Count : N0} files...", ConsoleColor . DarkGray ) ;
132- IProgressReporter progressReporter = verbose ? new ProgressReporter ( syncManager . RemoteVault , restoreFiles . Count ) : new LoggingProgressReporter ( syncManager . RemoteVault ) ;
133- int restoredFiles = await syncManager . RestoreFilesAsync ( restoreFiles . ToArray ( ) , progressReporter ) ;
134-
135- CommandLine . WriteLine ( syncManager . RemoteVault , $ "Successfully restored { restoredFiles : N0} files in { _sw . Elapsed } .", ConsoleColor . Green ) ;
149+ CommandLine . WriteLine ( syncManager . RemoteVault , $ "Pulling { restoreFiles . Count : N0} files...", ConsoleColor . DarkGray ) ;
150+ IProgressReporter progressReporter = verbose ? new ProgressReporter ( syncManager . RemoteVault , restoreFiles . Count ) : new LoggingProgressReporter ( syncManager . RemoteVault ) ;
151+ int pulledFiles = await syncManager . PullFilesAsync ( restoreFiles . ToArray ( ) , progressReporter ) ;
152+
153+ CommandLine . WriteLine ( syncManager . RemoteVault , $ "Successfully pulled { pulledFiles : N0} files in { _sw . Elapsed } .", ConsoleColor . Green ) ;
136154 await syncManager . DisconnectAsync ( ) ;
137155 }
138156 }
157+
158+ #region Add
159+
160+ private async Task HandleAddAsync ( string path , string ? config , string ? destination )
161+ {
162+ LocalVaultConfig ? localVault = string . IsNullOrEmpty ( config ) ? ParallelConfig . Load ( ) . Vaults . FirstOrDefault ( v => v . Enabled ) : ParallelConfig . GetVault ( config ) ;
163+ if ( ! string . IsNullOrEmpty ( config ) && localVault != null )
164+ {
165+ await AddPathAsync ( localVault , path , destination ) ;
166+ }
167+ else
168+ {
169+ await Program . Settings . ForEachVaultAsync ( vault => AddPathAsync ( vault , path , destination ) ) ;
170+ }
171+ }
172+
173+ private async Task AddPathAsync ( LocalVaultConfig vault , string path , string ? destination )
174+ {
175+ ISyncManager ? syncManager = SyncManager . CreateNew ( vault ) ;
176+ if ( syncManager == null || ! await syncManager . ConnectAsync ( ) )
177+ {
178+ CommandLine . WriteLine ( vault , "Failed to connect to vault!" , ConsoleColor . Red ) ;
179+ return ;
180+ }
181+
182+ PullRecord record = new ( path , destination ) ;
183+ if ( ! syncManager . RemoteVault . PullDirectories . Add ( record ) )
184+ {
185+ CommandLine . WriteLine ( vault , $ "Unable to add path: '{ path } '", ConsoleColor . Yellow ) ;
186+ return ;
187+ }
188+
189+ CommandLine . WriteLine ( vault , $ "Successfully added '{ path } '", ConsoleColor . Green ) ;
190+ await syncManager . DisconnectAsync ( ) ;
191+ }
192+
193+ #endregion
194+
195+ #region Remove
196+
197+ private async Task HandleRemoveAsync ( string path , string ? config )
198+ {
199+ LocalVaultConfig ? localVault = string . IsNullOrEmpty ( config ) ? ParallelConfig . Load ( ) . Vaults . FirstOrDefault ( v => v . Enabled ) : ParallelConfig . GetVault ( config ) ;
200+ if ( ! string . IsNullOrEmpty ( config ) && localVault != null )
201+ {
202+ await RemovePathAsync ( localVault , path ) ;
203+ }
204+ else
205+ {
206+ await Program . Settings . ForEachVaultAsync ( vault => RemovePathAsync ( vault , path ) ) ;
207+ }
208+ }
209+
210+ private async Task RemovePathAsync ( LocalVaultConfig vault , string path )
211+ {
212+ ISyncManager ? syncManager = SyncManager . CreateNew ( vault ) ;
213+ if ( syncManager == null || ! await syncManager . ConnectAsync ( ) )
214+ {
215+ CommandLine . WriteLine ( vault , "Failed to connect to vault!" , ConsoleColor . Red ) ;
216+ return ;
217+ }
218+
219+ IEnumerable < PullRecord > records = syncManager . RemoteVault . PullDirectories . Where ( r => r . Machine == Environment . MachineName && r . Source == path ) ;
220+ foreach ( PullRecord record in records )
221+ {
222+ if ( syncManager . RemoteVault . PullDirectories . Remove ( record ) ) continue ;
223+ CommandLine . WriteLine ( vault , $ "Unable to remove path: '{ path } '", ConsoleColor . Yellow ) ;
224+ }
225+
226+ CommandLine . WriteLine ( vault , $ "Successfully removed '{ path } '", ConsoleColor . Green ) ;
227+ await syncManager . DisconnectAsync ( ) ;
228+ }
229+
230+ #endregion
139231 }
140232}
0 commit comments