11using Renci . SshNet ;
22using Renci . SshNet . Sftp ;
3+ using System . Collections . Concurrent ;
34
45namespace SFTPSyncLib
56{
@@ -53,6 +54,94 @@ public RemoteSync(string host, string username, string password,
5354 } ) ;
5455 }
5556
57+ public RemoteSync ( string host , string username , string password ,
58+ string localRootDirectory , string remoteRootDirectory ,
59+ string searchPattern , SyncDirector director , List < string > ? excludedFolders , Task initialSyncTask )
60+ {
61+ _host = host ;
62+ _username = username ;
63+ _password = password ;
64+ _searchPattern = searchPattern ;
65+ _localRootDirectory = localRootDirectory ;
66+ _remoteRootDirectory = remoteRootDirectory ;
67+ _director = director ;
68+ _excludedFolders = excludedFolders ?? new List < string > ( ) ;
69+ _sftp = new SftpClient ( host , username , password ) ;
70+ _sftp . Connect ( ) ;
71+
72+ DoneMakingFolders = Task . CompletedTask ;
73+ DoneInitialSync = initialSyncTask ;
74+
75+ DoneInitialSync . ContinueWith ( ( tmp ) =>
76+ {
77+ _director . AddCallback ( searchPattern , ( args ) => Fsw_Changed ( null , args ) ) ;
78+ } ) ;
79+ }
80+
81+ public static async Task RunSharedInitialSyncAsync (
82+ string host ,
83+ string username ,
84+ string password ,
85+ string localRootDirectory ,
86+ string remoteRootDirectory ,
87+ string [ ] searchPatterns ,
88+ List < string > ? excludedFolders ,
89+ int workerCount )
90+ {
91+ if ( workerCount <= 0 || searchPatterns . Length == 0 )
92+ {
93+ return ;
94+ }
95+
96+ try
97+ {
98+ using ( var sftp = new SftpClient ( host , username , password ) )
99+ {
100+ sftp . Connect ( ) ;
101+ await CreateDirectoriesInternal ( sftp , localRootDirectory , localRootDirectory , remoteRootDirectory , excludedFolders ) ;
102+ }
103+ }
104+ catch ( Exception ex )
105+ {
106+ Logger . LogError ( $ "Failed to create directories. Exception: { ex . Message } ") ;
107+ throw ;
108+ }
109+
110+ var workQueue = new ConcurrentQueue < SyncWorkItem > ( ) ;
111+ foreach ( var pair in EnumerateLocalDirectories ( localRootDirectory , remoteRootDirectory , excludedFolders ) )
112+ {
113+ foreach ( var pattern in searchPatterns )
114+ {
115+ workQueue . Enqueue ( new SyncWorkItem ( pair . LocalPath , pair . RemotePath , pattern ) ) ;
116+ }
117+ }
118+
119+ var workers = new List < Task > ( ) ;
120+ for ( int i = 0 ; i < workerCount ; i ++ )
121+ {
122+ workers . Add ( Task . Run ( async ( ) =>
123+ {
124+ using ( var sftp = new SftpClient ( host , username , password ) )
125+ {
126+ sftp . Connect ( ) ;
127+ while ( workQueue . TryDequeue ( out var item ) )
128+ {
129+ try
130+ {
131+ await SyncDirectoryAsync ( sftp , item . LocalPath , item . RemotePath , item . SearchPattern ) ;
132+ }
133+ catch ( Exception ex )
134+ {
135+ Logger . LogError ( $ "Failed to sync { item . LocalPath } ({ item . SearchPattern } ). Exception: { ex . Message } ") ;
136+ }
137+ }
138+ }
139+ } ) ) ;
140+ }
141+
142+ await Task . WhenAll ( workers ) ;
143+ }
144+
56145 /// <summary>
57146 /// Sync changes for a file. This is only used for changes AFTER the initial sync has completed.
58147 /// </summary>
@@ -120,30 +209,7 @@ public static Task<IEnumerable<FileInfo>> SyncDirectoryAsync(SftpClient sftp, st
120209
121210 private string [ ] FilteredDirectories ( string localPath )
122211 {
123- return Directory . GetDirectories ( localPath ) . Where ( path =>
124- {
125- var relativePath = path . Substring ( _localRootDirectory . Length ) ;
126-
127- // Existing exclusions
128- bool isExcluded = relativePath . EndsWith ( ".git" )
129- || relativePath . EndsWith ( ".vs" )
130- || relativePath . EndsWith ( "bin" )
131- || relativePath . EndsWith ( "obj" )
132- || relativePath . Contains ( "." ) ;
133-
134- // Check _excludedFolders
135- if ( ! isExcluded && _excludedFolders != null && _excludedFolders . Count > 0 )
136- {
137- string fullPath = Path . GetFullPath ( path ) . TrimEnd ( Path . DirectorySeparatorChar ) ;
138- isExcluded = _excludedFolders . Any ( excluded =>
139- {
140- var excludedFullPath = Path . GetFullPath ( excluded ) . TrimEnd ( Path . DirectorySeparatorChar ) ;
141- return string . Equals ( fullPath , excludedFullPath , StringComparison . OrdinalIgnoreCase ) ;
142- } ) ;
143- }
144-
145- return ! isExcluded ;
146- } ) . ToArray ( ) ;
212+ return FilteredDirectories ( _localRootDirectory , localPath , _excludedFolders ) ;
147213 }
148214
149215 public async Task CreateDirectories ( string localPath , string remotePath )
@@ -155,30 +221,7 @@ public async Task CreateDirectories(string localPath, string remotePath)
155221 if ( ! EnsureConnectedSafe ( ) )
156222 return ;
157223
158- //Got local directories to sync
159- var localDirectories = FilteredDirectories ( localPath ) ;
160-
161- //Get remote directories
162- var remoteDirectories = ( await ListDirectoryAsync ( _sftp , remotePath ) ) . Where ( item => item . IsDirectory ) . ToDictionary ( item =>
163- {
164- if ( item . Name . Contains ( ".DIR" , StringComparison . OrdinalIgnoreCase ) )
165- return item . Name . Remove ( item . Name . IndexOf ( ".DIR" , StringComparison . OrdinalIgnoreCase ) ) ;
166- else
167- return item . Name ;
168- } ) ;
169-
170- //Compare local and remote directories, creating missing ones, and recurse for subdirectories
171- foreach ( var item in localDirectories )
172- {
173- var directoryName = item . Split ( Path . DirectorySeparatorChar ) . Last ( ) ;
174- if ( ! remoteDirectories . ContainsKey ( directoryName ) )
175- {
176- //Create new remote directory
177- _sftp . CreateDirectory ( remotePath + "/" + directoryName ) ;
178- }
179- //And recurse for any subdirectories it may need
180- await CreateDirectories ( localPath + "\\ " + directoryName , remotePath + "/" + directoryName ) ;
181- }
224+ await CreateDirectoriesInternal ( _sftp , _localRootDirectory , localPath , remotePath , _excludedFolders ) ;
182225 }
183226 catch ( Exception )
184227 {
@@ -224,6 +267,82 @@ public async Task InitialSync(string localPath, string remotePath)
224267 await SyncDirectoryAsync ( _sftp , localPath , remotePath , _searchPattern ) ;
225268 }
226269
270+ private static string [ ] FilteredDirectories ( string localRootDirectory , string localPath , List < string > ? excludedFolders )
271+ {
272+ return Directory . GetDirectories ( localPath ) . Where ( path =>
273+ {
274+ var relativePath = path . Substring ( localRootDirectory . Length ) ;
275+
276+ bool isExcluded = relativePath . EndsWith ( ".git" )
277+ || relativePath . EndsWith ( ".vs" )
278+ || relativePath . EndsWith ( "bin" )
279+ || relativePath . EndsWith ( "obj" )
280+ || relativePath . Contains ( "." ) ;
281+
282+ if ( ! isExcluded && excludedFolders != null && excludedFolders . Count > 0 )
283+ {
284+ string fullPath = Path . GetFullPath ( path ) . TrimEnd ( Path . DirectorySeparatorChar ) ;
285+ isExcluded = excludedFolders . Any ( excluded =>
286+ {
287+ var excludedFullPath = Path . GetFullPath ( excluded ) . TrimEnd ( Path . DirectorySeparatorChar ) ;
288+ return string . Equals ( fullPath , excludedFullPath , StringComparison . OrdinalIgnoreCase ) ;
289+ } ) ;
290+ }
291+
292+ return ! isExcluded ;
293+ } ) . ToArray ( ) ;
294+ }
295+
296+ private static IEnumerable < ( string LocalPath , string RemotePath ) > EnumerateLocalDirectories (
297+ string localRootDirectory ,
298+ string remoteRootDirectory ,
299+ List < string > ? excludedFolders )
300+ {
301+ var stack = new Stack < ( string LocalPath , string RemotePath ) > ( ) ;
302+ stack . Push ( ( localRootDirectory , remoteRootDirectory ) ) ;
303+
304+ while ( stack . Count > 0 )
305+ {
306+ var current = stack . Pop ( ) ;
307+ yield return current ;
308+
309+ foreach ( var directory in FilteredDirectories ( localRootDirectory , current . LocalPath , excludedFolders ) )
310+ {
311+ var directoryName = directory . Split ( Path . DirectorySeparatorChar ) . Last ( ) ;
312+ var remotePath = current . RemotePath + "/" + directoryName ;
313+ stack . Push ( ( directory , remotePath ) ) ;
314+ }
315+ }
316+ }
317+
318+ private static async Task CreateDirectoriesInternal (
319+ SftpClient sftp ,
320+ string localRootDirectory ,
321+ string localPath ,
322+ string remotePath ,
323+ List < string > ? excludedFolders )
324+ {
325+ var localDirectories = FilteredDirectories ( localRootDirectory , localPath , excludedFolders ) ;
326+
327+ var remoteDirectories = ( await ListDirectoryAsync ( sftp , remotePath ) ) . Where ( item => item . IsDirectory ) . ToDictionary ( item =>
328+ {
329+ if ( item . Name . Contains ( ".DIR" , StringComparison . OrdinalIgnoreCase ) )
330+ return item . Name . Remove ( item . Name . IndexOf ( ".DIR" , StringComparison . OrdinalIgnoreCase ) ) ;
331+ else
332+ return item . Name ;
333+ } ) ;
334+
335+ foreach ( var item in localDirectories )
336+ {
337+ var directoryName = item . Split ( Path . DirectorySeparatorChar ) . Last ( ) ;
338+ if ( ! remoteDirectories . ContainsKey ( directoryName ) )
339+ {
340+ sftp . CreateDirectory ( remotePath + "/" + directoryName ) ;
341+ }
342+ await CreateDirectoriesInternal ( sftp , localRootDirectory , localPath + "\\ " + directoryName , remotePath + "/" + directoryName , excludedFolders ) ;
343+ }
344+ }
345+
227346
228347 public static Task UploadFileAsync ( SftpClient sftp , Stream file , string destination )
229348 {
@@ -412,5 +531,19 @@ public void Dispose()
412531 _sftp . Dispose ( ) ;
413532 }
414533 }
534+
535+ private sealed class SyncWorkItem
536+ {
537+ public SyncWorkItem ( string localPath , string remotePath , string searchPattern )
538+ {
539+ LocalPath = localPath ;
540+ RemotePath = remotePath ;
541+ SearchPattern = searchPattern ;
542+ }
543+
544+ public string LocalPath { get ; }
545+ public string RemotePath { get ; }
546+ public string SearchPattern { get ; }
547+ }
415548 }
416549}
0 commit comments