读取本地目录结构并写入远程SFTP服务器的技术实现问询
Got it, let's solve this problem step by step. Your current code handles file uploads, but we need to extend it to preserve the full directory structure (including empty folders) when syncing to the remote SFTP server. Here's how to do it:
Step 1: Capture Local Directory Structure & File Relative Paths
First, we need to collect two things from your source directory:
- All subdirectories (relative to the root sending directory)
- Each file's full relative path (so we know exactly where to place it on the remote server)
Update your existing GetFiles method and add a new GetDirectories method:
// Get all subdirectories relative to the root directory public static IEnumerable<string> GetDirectories(string rootDir) { return Directory.EnumerateDirectories(rootDir, "*", SearchOption.AllDirectories) .Select(dir => Path.GetRelativePath(rootDir, dir)); } // Get files with their full relative path (instead of just filename) public static IEnumerable<FileInfo> GetFiles(string rootDir) { return Directory.EnumerateFiles(rootDir, "*", SearchOption.AllDirectories) .Select(path => { var relativePath = Path.GetRelativePath(rootDir, path); var stream = File.OpenRead(path); return new FileInfo(relativePath, stream); // Your custom FileInfo should use RelativePath here }) .DisposeEach(c => c.Content); }
Step 2: Add a Helper to Recursively Create SFTP Directories
SFTP clients typically don't support creating nested directories in one call (e.g., /folder1/folder2 won't work if /folder1 doesn't exist). Add this helper method to your remote writer class:
// Recursively create nested directories on SFTP private void CreateSftpDirectoryRecursive(SftpClient sftp, string fullRemoteDirPath) { // Split path into parts (SFTP uses forward slashes) var dirParts = fullRemoteDirPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); var currentPath = string.Empty; foreach (var dirPart in dirParts) { currentPath += $"/{dirPart}"; if (!sftp.Exists(currentPath)) { sftp.CreateDirectory(currentPath); } } }
Step 3: Update the SFTP Write Method to Handle Directories & Files
Modify your Write method to first create all required remote directories, then upload each file to its correct location:
public async Task Write(string localRootDir, IEnumerable<string> directories, IEnumerable<FileInfo> files) { using (var sftp = new SftpClient(this.sshInfo)) { sftp.Connect(); var baseRemoteDir = this.remoteDirectory; sftp.ChangeDirectory(baseRemoteDir); // First, create all required remote directories foreach (var localRelativeDir in directories) { // Convert Windows backslashes to SFTP forward slashes var remoteDirPath = Path.Combine(baseRemoteDir, localRelativeDir).Replace('\\', '/'); CreateSftpDirectoryRecursive(sftp, remoteDirPath); } // Then upload each file to its corresponding remote path foreach (var file in files) { var remoteFilePath = Path.Combine(baseRemoteDir, file.RelativePath).Replace('\\', '/'); // Double-check the parent directory exists (safety net) var remoteFileParentDir = Path.GetDirectoryName(remoteFilePath).Replace('\\', '/'); if (!sftp.Exists(remoteFileParentDir)) { CreateSftpDirectoryRecursive(sftp, remoteFileParentDir); } // Upload the file (ensure stream is disposed properly) using (var fileStream = file.Content) { sftp.UploadFile(fileStream, remoteFilePath); } } sftp.Disconnect(); } }
Step 4: Adjust the SendBatch Method to Use the New Logic
Update your SendBatch method to collect directories and files, then pass them to the updated Write method:
private async static Task SendBatch(Config config, Batch batch, IRemoteFileWriter writer) { var sendingDir = GetClientSendingDirectory(config, batch.ClientName); Directory.CreateDirectory(Path.GetDirectoryName(sendingDir)); Directory.Move(batch.LocalDirectory, sendingDir); Directory.CreateDirectory(batch.LocalDirectory); // Collect directories and files from the sending directory var directories = GetDirectories(sendingDir); var files = GetFiles(sendingDir); // Sync structure and files to remote await writer.Write(sendingDir, directories, files).ContinueWith(t => { if (t.IsCompletedSuccessfully) // Only archive if upload succeeded { var zipArchivePath = GetArchiveDirectory(config, batch); var zipFileName = $"{zipArchivePath} {DateTime.Now.ToString("yyyy-MM-dd HH mm ss")}.Zip"; ZipFile.CreateFromDirectory(sendingDir, zipFileName); } else if (t.IsFaulted) { // Handle upload failure (log error, retry, etc.) var exception = t.Exception?.InnerException ?? t.Exception; Console.WriteLine($"Upload failed: {exception.Message}"); } }); }
Key Notes to Keep in Mind
- Path Separators: Always convert Windows backslashes (
\) to SFTP forward slashes (/) — SFTP servers expect this format. - Empty Folders: The
GetDirectoriesmethod captures empty subfolders, so they'll be created on the remote server too. - Stream Disposal: We use
usingblocks for file streams to ensure they're properly released after upload. - Error Handling: Added a check for
t.IsFaultedinContinueWithto handle upload failures gracefully.
内容的提问来源于stack exchange,提问作者Ahhzeee

