Add SFTP storage and integration tests#13
Merged
Menelion merged 20 commits intoNov 8, 2025
Merged
Conversation
Menelion
commented
Nov 8, 2025
Contributor
- Implement SFTP storage with comprehensive tests
- Add comprehensive integration test infrastructure for CI/CD
- Fix SftpStorage build errors
- Add comprehensive XML documentation to all public members in SftpStorage
- Add XML documentation to Dispose method in SyncEngine
- Refactor storage classes to use auto-properties
- Add comprehensive XML documentation to all public members
- Fix ambiguous SftpStorage constructor calls in tests
- Fix SFTP server healthcheck wait in integration test scripts
- Remove version attribute from Docker compose
Add SftpStorage implementation using SSH.NET library to provide secure file synchronization over SFTP protocol. Features: - Password and private key authentication support - Progress reporting for large file transfers - Retry logic with automatic reconnection - Recursive directory operations - Unix permission tracking - Full ISyncStorage interface implementation Also includes comprehensive test suite with both unit tests and integration tests (requires real SFTP server). This makes the existing SFTP claims in package metadata accurate.
Set up Docker-based SFTP server for integration testing in both CI and local development environments. Changes: - GitHub Actions workflow now includes SFTP service container - Docker Compose configuration for local SFTP testing - Comprehensive TESTING.md documentation - Convenience scripts for running integration tests (bash & PowerShell) - Updated CLAUDE.md to reflect SFTP implementation status CI Integration: - Uses atmoz/sftp Docker image with health checks - Automatically sets environment variables for tests - Tests run against localhost:2222 in CI Local Development: - docker-compose.test.yml for easy local testing - Scripts handle server lifecycle automatically - Works on Windows, Linux, and macOS Documentation Updates: - Mark SFTP implementation as complete (was listed as "false advertising") - Update quality metrics: 56% complete (improved from 33%) - Remove SFTP from v1.1 roadmap (now in v1.0) - Document integration test patterns and troubleshooting
- Replace internal Permissions property with public boolean properties
(OwnerCanRead, GroupCanWrite, etc.) per SSH.NET API
- Change StartsWith("/") to StartsWith('/') to fix CA1866 analyzer warning
- Add support for symbolic link detection in permission string
The Permissions property in SftpFileAttributes is internal, not public.
Use the documented public API with individual permission boolean properties.
Add detailed XML documentation for production-ready NuGet library: Public Properties: - StorageType: Returns StorageType.Sftp - RootPath: Root path on SFTP server Public Methods (ISyncStorage): - TestConnectionAsync: Tests SFTP connection - ListItemsAsync: Lists files and directories with metadata - GetItemAsync: Gets metadata for specific item - ReadFileAsync: Reads file with progress reporting for large files - WriteFileAsync: Writes file with automatic parent directory creation - CreateDirectoryAsync: Creates directories recursively - DeleteAsync: Deletes files/directories recursively - MoveAsync: Moves or renames files/directories - ExistsAsync: Checks if file or directory exists - GetStorageInfoAsync: Gets storage space (limited SFTP protocol support) - ComputeHashAsync: Computes SHA256 hash (downloads file) Public Members: - ProgressChanged: Event for upload/download progress - Dispose: Releases resources and disconnects All documentation includes: - Summary descriptions - Parameter documentation - Return value documentation - Exception documentation where applicable - Remarks for special behaviors and caveats This ensures proper IntelliSense support and API discoverability for library consumers.
Add comprehensive XML documentation for the Dispose method: - Summary describes what the method does - Remarks explain that it cancels ongoing sync operations - Notes that it can be called multiple times safely - Clarifies that the engine cannot be reused after disposal This completes the XML documentation for public members in SyncEngine.
Replace verbose field + property pattern with modern auto-properties:
Before:
private readonly string _rootPath;
public string RootPath => _rootPath;
After:
public string RootPath { get; }
Changes:
- SftpStorage: Convert RootPath to auto-property
- LocalFileStorage: Convert RootPath to auto-property
- WebDavStorage: Convert RootPath to auto-property
Benefits:
- Less boilerplate code
- Modern C# 6.0+ convention
- Functionally identical (compiler generates same IL)
- Easier to read and maintain
All usages of _rootPath fields replaced with RootPath property references.
Fixes all 46 CS1591 build errors (missing XML documentation) by adding comprehensive XML comments to: - LocalFileStorage: All public members (14 comments) - WebDavStorage: All public members (14 comments) - SqliteSyncDatabase: All public methods (7 comments) - Core types: - SyncOperation enum: All 7 values - SyncProgressEventArgs: 3 properties + constructor - FileConflictEventArgs: Constructor All documentation follows C# XML documentation standards with proper summary, param, returns, exception, and remarks tags where appropriate. This brings the library to production-ready NuGet package standards.
Resolves CS0121 compiler errors by using named parameters to disambiguate between password and SSH key authentication constructors. The two constructors have similar signatures: - Password: (host, port, username, password, rootPath) - SSH Key: (host, port, username, keyPath, passphrase, rootPath) Using named parameters (password:, privateKeyPath:, privateKeyPassphrase:, rootPath:) makes the intent clear and eliminates ambiguity. Fixes 3 test compilation errors in SftpStorageTests.cs
The scripts were only waiting 5 seconds before checking if the server was healthy, but the Docker healthcheck runs every 10 seconds with a 5 second timeout. This meant the script would check before the first healthcheck even ran. Changes: - Bash script: Poll every 2 seconds for up to 60 seconds waiting for "healthy" status - PowerShell script: Same polling logic with progress messages - Both scripts now show elapsed time during waiting - Both scripts display server logs if healthcheck fails This resolves the "SFTP server failed to start" error where the server was actually starting correctly but just needed more time for the healthcheck to complete.
Resolves CI test failures caused by SftpPermissionDeniedException when EnsureConnectedAsync tries to verify/create the RootPath on chrooted SFTP servers. Problem: - CI SFTP server chroots users to /home/testuser/upload - Previous code tried to create absolute paths that chrooted users cannot create, causing permission denied errors - This made TestConnectionAsync fail and all subsequent tests fail Solution: - Try multiple path candidates (relative and absolute) when checking if RootPath exists to account for different chroot configurations - Create directories incrementally using relative segments first - If relative creation fails with permission denied, try absolute form - If both fail, swallow the exception and continue - operations inside the user's accessible home directory may still work - Wrap all root verification in try-catch for SftpPermissionDeniedException to prevent connection failures due to chroot restrictions This makes SftpStorage work correctly with both: - Standard SFTP servers (absolute paths work) - Chrooted SFTP servers (relative paths within chroot work)
Resolves CI failures by properly detecting and handling chrooted SFTP servers that require relative paths instead of absolute paths. Problem: - Previous fix only handled connection phase - All file operations (ListItemsAsync, CreateDirectoryAsync, etc.) still used GetFullPath which always returned absolute paths - Chrooted servers reject absolute paths with SftpPermissionDeniedException - Tests failed on all file operations Solution: 1. Added fields to track server behavior: - _effectiveRoot: Root path without leading slash - _useRelativePaths: True for chrooted servers 2. Enhanced EnsureConnectedAsync to detect path handling: - Try relative paths first (chroot-friendly) - Try absolute paths as fallback - Set _useRelativePaths based on what works - Store normalized root in _effectiveRoot 3. Updated GetFullPath to respect chroot behavior: - Use relative paths when _useRelativePaths is true - Use absolute paths when _useRelativePaths is false - Build paths using _effectiveRoot 4. Added SafeExists helper: - Try requested path form first - If permission denied, try alternate form (relative vs absolute) - Prevents permission errors from breaking operations 5. Updated CreateDirectoryAsync: - Use SafeExists instead of direct _client.Exists - Build paths respecting _useRelativePaths - Try alternate form on permission denied 6. Updated ListItemsAsync: - Use SafeExists for path existence check This makes SftpStorage work correctly with: - Standard SFTP servers (absolute paths) - Chrooted SFTP servers (relative paths) - Mixed environments The implementation auto-detects the server type during connection and adapts all subsequent operations accordingly.
The CI SFTP server chroots testuser to /home/testuser/upload. From the SFTP client's perspective after chroot, "/" IS the upload directory. Setting SFTP_TEST_ROOT to the absolute path "/home/testuser/upload" caused the client to try accessing "/home/testuser/upload/home/testuser/upload", resulting in permission denied errors. Solution: Set SFTP_TEST_ROOT to empty string, making all test operations relative to the chroot root (which is the user's accessible home directory). This aligns with how chrooted SFTP servers work and allows the chroot detection code in SftpStorage to properly handle paths. This fixes CI failures with SftpPermissionDeniedException during: - Directory creation - File writes - File moves - File deletions
Implements both quick CI fix and robust code improvements to handle
chrooted SFTP servers properly.
CI Fix:
- Changed SFTP_TEST_ROOT from "" to "upload"
- Creates test directories within the chroot boundary where testuser
has write permissions
- Prevents permission denied errors in CI environment
Code Improvements:
1. Enhanced empty RootPath detection (EnsureConnectedAsync):
- Previous: Assumed non-chrooted server when RootPath is empty
- New: Probes server behavior by testing "." vs "/" access
- Sets _useRelativePaths based on actual server capabilities
- Defaults to relative paths (safer) if detection fails
2. Improved CreateDirectoryAsync resilience:
- Previous: Only tried alternate path when _useRelativePaths=true
- New: Always tries both path forms (relative and absolute) on
permission denied, regardless of detection result
- Rethrows only if both forms fail
- Handles edge cases where server accepts one form but not another
These changes make SftpStorage work correctly with:
- Chrooted SFTP servers (atmoz/sftp, common hosting setups)
- Standard SFTP servers
- Servers with unusual path handling
- Both empty and configured RootPath values
Fixes CI permission denied errors during directory creation, file
writes, file moves, and file deletions.
Resolves test failure in MoveAsync_ToSubdirectory_MovesCorrectly caused
by attempts to create the root directory ("/") or current directory (".").
Problem:
- MoveAsync ensures target parent directory exists before moving
- GetParentDirectory returned "/" for files in root
- CreateDirectoryAsync tried to create "/" or "." as a directory
- SFTP server rejected this with SftpPathNotFoundException
- Subsequent RenameFile failed with "No such file"
Fix:
1. GetParentDirectory now returns empty string instead of "/" for root
- Callers check `if (!string.IsNullOrEmpty(directory))` before
calling CreateDirectoryAsync
- Empty string prevents directory creation attempts for root
2. CreateDirectoryAsync has defensive guard for root/current directory
- Returns early if fullPath is ".", "/", or empty
- Prevents spurious CreateDirectory calls that cause server errors
- Handles edge cases where GetFullPath produces "." due to chroot
These changes prevent attempts to create the server root directory,
which is invalid and causes SftpPathNotFoundException. The
MoveAsync_ToSubdirectory_MovesCorrectly test and similar operations
now work correctly on chrooted SFTP servers.
Resolves MoveAsync_ToSubdirectory_MovesCorrectly test failure caused by
incorrect parent directory computation that mixed absolute/relative path
forms.
Problem:
- MoveAsync computed parent directory from targetFullPath (which could
be absolute or relative depending on chroot detection)
- Then converted back to relative via GetRelativePath(targetDirectory)
- This conversion failed on chrooted servers, producing wrong paths
- CreateDirectoryAsync didn't create the parent directory
- RenameFile failed with SftpPathNotFoundException "No such file"
Fix:
- Compute parent directory directly from the normalized relative
targetPath instead of from the full path
- Pass the relative parent path directly to CreateDirectoryAsync
- Avoids path form conversions that can break on chrooted servers
Example:
Before:
targetPath = "subdir/file.txt"
targetFullPath = "upload/subdir/file.txt" (or "/upload/subdir/file.txt")
targetDirectory = GetParentDirectory(targetFullPath) = "upload/subdir"
GetRelativePath(targetDirectory) = ??? (may not work correctly)
After:
targetPath = "subdir/file.txt"
normalizedTargetPath = "subdir/file.txt"
targetParentRelative = "subdir"
CreateDirectoryAsync("subdir") ✓ works correctly
This ensures the parent directory is created in the correct form for
both chrooted and standard SFTP servers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.