Skip to content

[sftp] add support#8

Merged
capcom6 merged 1 commit into
masterfrom
sftp/add-support
May 28, 2026
Merged

[sftp] add support#8
capcom6 merged 1 commit into
masterfrom
sftp/add-support

Conversation

@capcom6
Copy link
Copy Markdown
Owner

@capcom6 capcom6 commented May 18, 2026

Summary by CodeRabbit

  • New Features

    • SFTP support added alongside FTP with password, SSH key, and SSH agent authentication; scheme-aware client selection and improved remote path handling.
  • Documentation

    • README and CLI help updated with FTP/SFTP URL formats, auth order, usage examples, default ports, and security guidance.
  • Tests

    • Added URL path parsing tests.
  • Chores

    • Bumped minimum Go to 1.25.0 and updated dependencies.

Review Change Stack

Comment thread internal/client/sftp/sftp.go Fixed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

🤖 Pull request artifacts

Platform File
🐳 Docker GitHub Container Registry
🍎 Darwin arm64 sftp-sync_Darwin_arm64.tar.gz
🍎 Darwin x86_64 sftp-sync_Darwin_x86_64.tar.gz
🐧 Linux arm64 sftp-sync_Linux_arm64.tar.gz
🐧 Linux i386 sftp-sync_Linux_i386.tar.gz
🐧 Linux x86_64 sftp-sync_Linux_x86_64.tar.gz
🪟 Windows arm64 sftp-sync_Windows_arm64.zip
🪟 Windows i386 sftp-sync_Windows_i386.zip
🪟 Windows x86_64 sftp-sync_Windows_x86_64.zip

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16d58aa3-faae-452c-8c88-75d7234304be

📥 Commits

Reviewing files that changed from the base of the PR and between d50cab7 and 09f9be6.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • .golangci.yml
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/ftp/ftp_test.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/config.go
  • internal/client/sftp/sftp.go
  • internal/client/types/errors.go
  • main.go
✅ Files skipped from review due to trivial changes (3)
  • main.go
  • .golangci.yml
  • README.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • internal/client/types/errors.go
  • internal/cli/commands/sync/sync.go
  • go.mod
  • internal/client/sftp/config.go
  • internal/client/sftp/auth.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/sftp/sftp.go

Walkthrough

Adds SFTP protocol support alongside FTP: new sftp client, auth/config parsing, refactored FTP client into an ftp package, centralized error types, scheme-based client factory, CLI/docs updates, and dependency/tooling bumps.

Changes

SFTP protocol support and FTP refactor

Layer / File(s) Summary
Shared error types foundation
internal/client/types/errors.go
Error variables moved to types package; ErrInvalidParams and ErrInvalidPath added alongside ErrUnsupportedScheme and ErrClientIsNil.
Dependency and tooling updates
go.mod, .golangci.yml
Go toolchain bumped to 1.25.0; SFTP (github.com/pkg/sftp v1.13.10) and crypto (golang.org/x/crypto v0.51.0) dependencies added/updated; exhaustruct linter configured to exclude SSH Config types.
Client factory scheme routing
internal/client/client.go
New function imports ftp/sftp packages and routes by scheme: ftp://ftp.NewClient, sftp://sftp.NewClient; unsupported schemes return an error wrapping types.ErrUnsupportedScheme.
FTP client refactor into ftp package
internal/client/ftp/ftp.go
FTP client moved to dedicated package: type FtpClientClient, constructor renamed to NewClient, all exported methods updated to *Client receivers, init validates scheme and conditionally applies ChangeDir, ping simplified to health check only, operations call init and use types errors.
FTP URL path parsing tests
internal/client/ftp/ftp_test.go
Table-driven parallel test verifying URL path extraction and encoding/port/trailing-slash cases.
SFTP SSH authentication helpers
internal/client/sftp/auth.go
AuthConfig struct with password/key path/passphrase/agent fields; URL parsing for credentials and query params; SSH agent connection with timeout; private key loading with tilde expansion and passphrase support; default key discovery in ~/.ssh; sentinel for encrypted keys without passphrase.
SFTP connection config parsing
internal/client/sftp/config.go
SFTP Config struct with host, port, username, auth, and connect timeout; ParseConfigFromURL validates URL structure, defaults port to 22, parses optional timeout query param, populates auth.
SFTP client implementation
internal/client/sftp/sftp.go
New SFTP Client with lazy SSH/SFTP connection (mutex-guarded), known_hosts validation, basePath/resolvePath, connect with timeout, MakeDir with intermediate creation, recursive RemoveDir, UploadFile streaming, RemoveFile, Remove dispatch, and ignorable-error helpers.
Documentation updates
README.md
Tagline and features describe FTP or SFTP support; usage examples split into labeled FTP and SFTP (password/key/agent) commands with port notes; --dest option docs broadened to include FTP/SFTP URLs, query params, auth order, and CLI secret security warning; Roadmap marks SFTP complete.
CLI help text updates
main.go, internal/cli/commands/sync/sync.go
main.go and sync command help text updated to advertise FTP or SFTP server support and include SFTP URL examples with port numbers.

Sequence Diagram(s)

sequenceDiagram
  participant Factory as Client Factory
  participant FTPClient as ftp.Client
  participant SFTPClient as sftp.Client
  participant SSH as SSH (net/ssh)
  Factory->>Factory: Parse destination URL
  alt scheme == ftp
    Factory->>FTPClient: NewClient(url, logger)
    FTPClient->>FTPClient: init() — dial FTP, login, ChangeDir if needed
  else scheme == sftp
    Factory->>SFTPClient: NewClient(url, logger)
    SFTPClient->>SFTPClient: init() on first operation (ping)
    SFTPClient->>SSH: Dial TCP, authenticate (agent/key/password), create SFTP session
    SSH->>SFTPClient: return *sftp.Client
  else unsupported
    Factory->>Factory: Return wrapped ErrUnsupportedScheme
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • capcom6/sftp-sync#5: Both PRs modify internal/client/client.go’s New(...) factory: the retrieved PR updates its signature and FTP logger wiring, and this PR extends factory routing to include sftp.
  • capcom6/sftp-sync#4: Also touches internal/client/client.go’s New() URL parsing/unsupported-scheme wrapping; related to factory/error messaging changes.

Suggested labels

codex

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title '[sftp] add support' is vague and generic, using non-descriptive terms that do not clearly convey the main change or scope. Improve the title to be more specific about what SFTP support entails, such as 'Add SFTP protocol support alongside FTP' or 'Implement SFTP client for remote synchronization'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sftp/add-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

77-82: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update About section to include SFTP support.

Line 81 still says “remote FTP server,” which conflicts with the updated FTP+SFTP positioning in the rest of the README.

Suggested patch
-sftp-sync is a command-line utility for syncing a local folder with a remote FTP server on every change of files or directories.
+sftp-sync is a command-line utility for syncing a local folder with a remote FTP or SFTP server on every change of files or directories.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 77 - 82, The About section still calls out "remote
FTP server"; update the text under "About The Project" (the sftp-sync project
description) to reflect SFTP support by replacing that phrase with a combined
term such as "remote FTP or SFTP server" or "remote FTP/SFTP server" so it
matches the README's FTP+SFTP positioning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/sftp/sftp.go`:
- Around line 70-72: The current parsing in sftp.go that checks if
strings.Contains(host, ":") and then does strings.Cut(host, ":") incorrectly
splits IPv6 addresses; replace that logic by using the URL object's helpers:
call u.Hostname() to get the host (with IPv6 brackets removed) and u.Port() to
get the port string (or blank if none) and assign to the existing host and port
variables (fall back to default port if u.Port() is empty). Update references
around variables named host, port and the URL variable u to use these methods
rather than manual string slicing.
- Around line 159-166: The recursive delete in removeDirRecur is listing the
parent directory because it calls c.sftp.ReadDirContext(ctx, path.Dir(p))
instead of the target path; update the ReadDirContext call in
Client.removeDirRecur to use p (the directory being removed) so entries are read
from the correct directory, and ensure any subsequent path joins or recursive
calls (inside removeDirRecur) continue to operate on p and its child paths
rather than path.Dir(p).
- Line 82: Replace the insecure HostKeyCallback currently set to
ssh.InsecureIgnoreHostKey() in the SSH client configuration with a known-hosts
based callback using knownhosts.New. Import golang.org/x/crypto/ssh/knownhosts
and build the known_hosts path (e.g., filepath.Join(os.UserHomeDir(), ".ssh",
"known_hosts")), call knownhosts.New(...) to get the callback, handle any error
(return or log) from knownhosts.New, and set the config's HostKeyCallback to the
returned callback instead of ssh.InsecureIgnoreHostKey(); update imports to
include os and path/filepath as needed.
- Around line 274-279: Update the error-matching logic around the notFoundErrors
slice: remove the overly broad "failure" entry and the case-sensitive "No such
file", normalize the error string with strings.ToLower(err.Error()) before
comparing, and additionally check os.IsNotExist(err) to reliably detect
file-not-found cases; i.e., keep only specific patterns like "does not exist",
"not found", "no such file" in notFoundErrors, use strings.ToLower when scanning
those patterns, and short-circuit to treat the error as a not-found only if
strings match or os.IsNotExist(err) returns true (do not suppress permission or
connection errors).

---

Outside diff comments:
In `@README.md`:
- Around line 77-82: The About section still calls out "remote FTP server";
update the text under "About The Project" (the sftp-sync project description) to
reflect SFTP support by replacing that phrase with a combined term such as
"remote FTP or SFTP server" or "remote FTP/SFTP server" so it matches the
README's FTP+SFTP positioning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a61725ef-8cc6-495c-be81-e98004aebbec

📥 Commits

Reviewing files that changed from the base of the PR and between 5131d20 and 39cc868.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • .golangci.yml
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/sftp/sftp.go
  • internal/client/types/errors.go
  • main.go

Comment thread internal/client/sftp/sftp.go Outdated
Comment thread internal/client/sftp/sftp.go Outdated
Comment thread internal/client/sftp/sftp.go
Comment thread internal/client/sftp/sftp.go
@capcom6 capcom6 force-pushed the sftp/add-support branch 2 times, most recently from 533b055 to bb43881 Compare May 20, 2026 07:22
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/client/ftp/ftp.go (1)

76-83: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close and discard partially initialized FTP sessions on setup failure.

If Login or ChangeDir fails, the new connection remains assigned to c.client. A later init() can pass ping() and skip setup, causing operations to run in the wrong remote directory.

Suggested fix
-	c.client, err = ftp.Dial(u.Host, ftp.DialWithContext(ctx))
+	conn, err := ftp.Dial(u.Host, ftp.DialWithContext(ctx))
 	if err != nil {
 		return fmt.Errorf("can't connect to %s: %w", u.Host, err)
 	}
@@
-	if loginErr := c.client.Login(u.User.Username(), password); loginErr != nil {
+	if loginErr := conn.Login(u.User.Username(), password); loginErr != nil {
+		_ = conn.Quit()
 		return fmt.Errorf("can't login as %s: %w", u.User.Username(), loginErr)
 	}
 
 	if u.Path != "" && u.Path != "/" {
-		if chErr := c.client.ChangeDir(u.Path); chErr != nil {
+		if chErr := conn.ChangeDir(u.Path); chErr != nil {
+			_ = conn.Quit()
 			return fmt.Errorf("remote path %s does not exist or is not accessible: %w", u.Path, chErr)
 		}
 	}
+
+	c.client = conn
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/ftp/ftp.go` around lines 76 - 83, When setup fails during ftp
session initialization (calls to c.client.Login or c.client.ChangeDir), ensure
the partially-initialized c.client is closed and discarded so subsequent
init()/ping() do not reuse it; on both the Login failure branch and the
ChangeDir failure branch call c.client.Close() (handle/ignore Close() error as
appropriate) and then set c.client = nil before returning the wrapped error from
Login or ChangeDir, so the client state is cleaned up on setup failure.
🧹 Nitpick comments (3)
internal/client/sftp/sftp_test.go (1)

49-69: ⚡ Quick win

Test currently validates duplicated logic, not implementation behavior.

TestURLPathValidationRules re-derives the rule inline, so it won’t catch regressions in the actual SFTP path-handling code. Please assert this through the real path-validation/dir-selection behavior (or a helper used by production code) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/sftp_test.go` around lines 49 - 69, The test is
re-deriving the path rule instead of exercising the real code; update
TestURLPathValidationRules to call the actual production path-validation or
dir-selection code (replace the inline computation `shouldUse := tt.path != ""
&& tt.path != "/"` in TestURLPathValidationRules with a call to the
helper/function used by the SFTP client to decide whether to change directories)
and assert that its return matches tt.shouldUse; if the decision is encapsulated
in a method on the SFTP client, construct the minimal client or invoke the
exported helper so the test validates implementation behavior rather than
duplicated logic.
internal/client/sftp/sftp.go (2)

92-96: 💤 Low value

Consider adding a connection timeout.

The SSH ClientConfig lacks a Timeout field. While OS-level TCP timeouts exist, an explicit timeout provides predictable behavior and prevents indefinite hangs against unresponsive servers.

♻️ Proposed fix
 	config := &ssh.ClientConfig{
 		User:            username,
 		Auth:            authMethods,
 		HostKeyCallback: hostKeyCallback,
+		Timeout:         30 * time.Second,
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/sftp.go` around lines 92 - 96, The ssh.ClientConfig
named config currently has no connection timeout; to avoid hangs, implement a
TCP-level timeout by creating a net.Dialer with a reasonable Timeout and use
dialer.Dial to open the TCP connection and then wrap it with ssh.NewClientConn
and ssh.NewClient (instead of calling ssh.Dial). Update the code path that uses
config (the SFTP connection setup / connect function where config is passed) to
use this dialer-based flow so the SSH handshake respects the configured timeout.

81-84: ⚡ Quick win

Use os.UserHomeDir() for cross-platform compatibility.

os.Getenv("HOME") doesn't work on Windows and may be empty in some environments. os.UserHomeDir() handles platform differences correctly.

♻️ Proposed fix
-	hostKeyCallback, err := knownhosts.New(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
+	home, err := os.UserHomeDir()
+	if err != nil {
+		return fmt.Errorf("can't determine home directory: %w", err)
+	}
+	hostKeyCallback, err := knownhosts.New(filepath.Join(home, ".ssh", "known_hosts"))
 	if err != nil {
 		return fmt.Errorf("can't load known_hosts: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/sftp.go` around lines 81 - 84, Replace the use of
os.Getenv("HOME") when building the known_hosts path with os.UserHomeDir() and
handle its error: call os.UserHomeDir(), check for an error, then use
filepath.Join(userHome, ".ssh", "known_hosts") when calling knownhosts.New (the
hostKeyCallback initialization). Ensure any error from os.UserHomeDir() is
returned (similar to the existing error handling around knownhosts.New) so the
function fails cleanly if the home directory can't be resolved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/sftp/auth.go`:
- Around line 84-100: The buildAgentSigners function leaks the agent socket
connection because conn is never closed; after successfully dialing the SSH
agent in buildAgentSigners (the conn returned by dialer.DialContext) defer
conn.Close() immediately (or otherwise close conn before each return) so the
file descriptor is released; ensure both the success path (after agent.NewClient
and Signers()) and the error paths close the connection by placing the defer
right after the dial succeeds.

---

Outside diff comments:
In `@internal/client/ftp/ftp.go`:
- Around line 76-83: When setup fails during ftp session initialization (calls
to c.client.Login or c.client.ChangeDir), ensure the partially-initialized
c.client is closed and discarded so subsequent init()/ping() do not reuse it; on
both the Login failure branch and the ChangeDir failure branch call
c.client.Close() (handle/ignore Close() error as appropriate) and then set
c.client = nil before returning the wrapped error from Login or ChangeDir, so
the client state is cleaned up on setup failure.

---

Nitpick comments:
In `@internal/client/sftp/sftp_test.go`:
- Around line 49-69: The test is re-deriving the path rule instead of exercising
the real code; update TestURLPathValidationRules to call the actual production
path-validation or dir-selection code (replace the inline computation `shouldUse
:= tt.path != "" && tt.path != "/"` in TestURLPathValidationRules with a call to
the helper/function used by the SFTP client to decide whether to change
directories) and assert that its return matches tt.shouldUse; if the decision is
encapsulated in a method on the SFTP client, construct the minimal client or
invoke the exported helper so the test validates implementation behavior rather
than duplicated logic.

In `@internal/client/sftp/sftp.go`:
- Around line 92-96: The ssh.ClientConfig named config currently has no
connection timeout; to avoid hangs, implement a TCP-level timeout by creating a
net.Dialer with a reasonable Timeout and use dialer.Dial to open the TCP
connection and then wrap it with ssh.NewClientConn and ssh.NewClient (instead of
calling ssh.Dial). Update the code path that uses config (the SFTP connection
setup / connect function where config is passed) to use this dialer-based flow
so the SSH handshake respects the configured timeout.
- Around line 81-84: Replace the use of os.Getenv("HOME") when building the
known_hosts path with os.UserHomeDir() and handle its error: call
os.UserHomeDir(), check for an error, then use filepath.Join(userHome, ".ssh",
"known_hosts") when calling knownhosts.New (the hostKeyCallback initialization).
Ensure any error from os.UserHomeDir() is returned (similar to the existing
error handling around knownhosts.New) so the function fails cleanly if the home
directory can't be resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a065f693-732f-40b6-a4dd-d0c5dc4050b2

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6f0ea and bb43881.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • .golangci.yml
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/ftp/ftp_test.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/sftp.go
  • internal/client/sftp/sftp_test.go
  • internal/client/types/errors.go
  • main.go
✅ Files skipped from review due to trivial changes (4)
  • go.mod
  • internal/cli/commands/sync/sync.go
  • .golangci.yml
  • README.md

Comment thread internal/client/sftp/auth.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/client/ftp/ftp.go (1)

71-78: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard u.User before calling Password() / Username() in FTP login to avoid nil panic.

internal/client/ftp/ftp.go dereferences u.User.Password() and u.User.Username() (for conn.Login and error formatting) without any u.User == nil guard; a URL without userinfo (e.g., ftp://host/path) can therefore panic during connection initialization. Add a validation check and return an invalid-params error consistent with existing error handling in this package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/ftp/ftp.go` around lines 71 - 78, Guard against a nil User
before calling Password() and Username(): check if u.User == nil at the start of
the FTP login sequence in internal/client/ftp/ftp.go and return the package's
standard "invalid-params" error (consistent with existing error handling)
instead of proceeding; then use local username/password variables (from
u.User.Username() and u.User.Password()) when calling conn.Login and when
formatting the login error so no nil dereference can occur during conn.Login or
the error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/sftp/sftp.go`:
- Around line 83-97: Before constructing ssh.ClientConfig and calling ssh.Dial,
validate that ParseConfigFromURL returned non-empty config.Host and
config.Username and return types.ErrInvalidParams if either is empty;
specifically, check config.Host and config.Username (derived by
ParseConfigFromURL from u.Hostname() and u.User.Username()), and only proceed to
build ssh.ClientConfig and call ssh.Dial(net.JoinHostPort(config.Host,
config.Port), sshConfig) when both are present — otherwise return
types.ErrInvalidParams to fail fast on malformed SFTP URLs.

---

Outside diff comments:
In `@internal/client/ftp/ftp.go`:
- Around line 71-78: Guard against a nil User before calling Password() and
Username(): check if u.User == nil at the start of the FTP login sequence in
internal/client/ftp/ftp.go and return the package's standard "invalid-params"
error (consistent with existing error handling) instead of proceeding; then use
local username/password variables (from u.User.Username() and u.User.Password())
when calling conn.Login and when formatting the login error so no nil
dereference can occur during conn.Login or the error path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f717e7f-d1d6-40ec-8c26-fbec69f16df0

📥 Commits

Reviewing files that changed from the base of the PR and between bb43881 and c1388a1.

📒 Files selected for processing (5)
  • internal/client/ftp/ftp.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/config.go
  • internal/client/sftp/sftp.go
  • internal/client/sftp/sftp_test.go
💤 Files with no reviewable changes (1)
  • internal/client/sftp/sftp_test.go

Comment thread internal/client/sftp/sftp.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/ftp/ftp.go`:
- Around line 71-80: The code currently skips Login when u.User is nil which
allows init to succeed unauthenticated and fail later; change the logic in the
FTP initialization (the block referencing u.User, conn.Login and conn.Quit) to
fail fast: if u.User is nil either perform an explicit anonymous login via
conn.Login("anonymous","") or return a clear error immediately (e.g., "missing
FTP credentials"), and ensure you still call conn.Quit() on error and wrap the
login error (as with fmt.Errorf("can't login as %s: %w", ...)) for consistent
diagnostics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34107b97-ff55-4dce-8436-f765d5143714

📥 Commits

Reviewing files that changed from the base of the PR and between c1388a1 and 2a50167.

📒 Files selected for processing (3)
  • internal/client/ftp/ftp.go
  • internal/client/sftp/config.go
  • internal/client/sftp/sftp.go

Comment thread internal/client/ftp/ftp.go Outdated
@capcom6 capcom6 force-pushed the sftp/add-support branch from 6fc36ca to a2abc89 Compare May 22, 2026 00:41
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/client/sftp/sftp.go (1)

306-325: 💤 Low value

Consider case-insensitive matching and os.IsNotExist() for robustness.

The "failure" pattern is correctly removed. For additional robustness, consider normalizing error strings with strings.ToLower() and adding os.IsNotExist() check for wrapped filesystem errors:

♻️ Suggested improvement
 func isIgnorableError(err error) bool {
     if err == nil {
         return true
     }
+    if os.IsNotExist(err) {
+        return true
+    }

-    errStr := err.Error()
+    errStr := strings.ToLower(err.Error())
     notFoundErrors := []string{
         "does not exist",
         "not found",
-        "No such file",
+        "no such file",
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/sftp.go` around lines 306 - 325, The isIgnorableError
function only checks error string substrings case-sensitively; update it to
normalize the error text with strings.ToLower before matching and add a
filesystem-aware check using os.IsNotExist(err) (which also handles wrapped
errors) so that the notFoundErrors slice (used in isIgnorableError) compares
against lowercase patterns and the function returns true when os.IsNotExist(err)
is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/sftp/config.go`:
- Around line 18-21: ParseConfigFromURL dereferences the incoming *url.URL (u)
without a nil check which can cause a panic; update ParseConfigFromURL to first
validate that u != nil and if nil return a typed validation error (e.g.,
wrap/return types.ErrInvalidParams with a clear message like "sftp URL is nil"),
then proceed to use u.Hostname() and construct Config as before so callers
receive a proper error instead of a panic.

In `@internal/client/sftp/sftp.go`:
- Around line 93-102: The ssh.Dial call in the sftp connection path can hang
because it lacks a TCP connect timeout; replace the single ssh.Dial(...) usage
with an explicit net.DialTimeout to establish the TCP connection (using
config.Host and config.Port) and then pass that net.Conn into ssh.NewClientConn
along with the existing ssh.ClientConfig to create the SSH client; update where
conn is created/used (currently from ssh.Dial) to handle the net.DialTimeout
error, close the underlying net.Conn on error, and ensure the timeout value is
configurable or set to a sensible default.

---

Nitpick comments:
In `@internal/client/sftp/sftp.go`:
- Around line 306-325: The isIgnorableError function only checks error string
substrings case-sensitively; update it to normalize the error text with
strings.ToLower before matching and add a filesystem-aware check using
os.IsNotExist(err) (which also handles wrapped errors) so that the
notFoundErrors slice (used in isIgnorableError) compares against lowercase
patterns and the function returns true when os.IsNotExist(err) is true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a220454f-15db-4795-9277-e032f1cc7f37

📥 Commits

Reviewing files that changed from the base of the PR and between 6fc36ca and a2abc89.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • .golangci.yml
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/ftp/ftp_test.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/config.go
  • internal/client/sftp/sftp.go
  • internal/client/sftp/sftp_test.go
  • internal/client/types/errors.go
  • main.go
✅ Files skipped from review due to trivial changes (2)
  • internal/cli/commands/sync/sync.go
  • README.md

Comment thread internal/client/sftp/config.go
Comment thread internal/client/sftp/sftp.go Outdated
@capcom6 capcom6 force-pushed the sftp/add-support branch from 8ef1868 to 36ef97c Compare May 24, 2026 08:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
internal/client/sftp/sftp_test.go (1)

8-47: ⚡ Quick win

Point these cases at the project code, not net/url.

This table mostly re-verifies url.Parse, so it won't catch regressions in ParseConfigFromURL, ParseAuthFromURL, or the base-path handling added in Client. Reusing the same inputs against your parser/helper layer would give this test real coverage of the PR behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/sftp_test.go` around lines 8 - 47, The test currently
only exercises net/url.Parse; update it to call the project parsing helpers so
it verifies our behavior: replace the direct url.Parse usage in
TestURLPathExtraction with calls to ParseConfigFromURL (and where relevant
ParseAuthFromURL) and/or instantiate a Client (to validate its base-path
handling) using the same rawURL inputs, then assert the returned config.Path (or
client.BasePath) and auth fields equal the expected wantPath and expected auth
values; keep the existing table of cases but assert against
ParseConfigFromURL/ParseAuthFromURL/Client behavior instead of u.Path so
regressions in our parsing and base-path logic are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@go.mod`:
- Line 3: The README's prerequisites still list Go 1.24.3 while the module
requires Go 1.25.0; update the documented minimum Go version in the README (and
any other docs or CI/readme snippets) to 1.25.0 so it matches the go.mod
requirement and avoid setup confusion.

In `@internal/client/ftp/ftp.go`:
- Around line 82-85: The failure branch that handles changing to the remote base
path (where conn.ChangeDir(u.Path) returns chErr) needs to wrap the returned
error with the sentinel types.ErrInvalidPath so callers can detect invalid
configured paths; update the error return in the ftp.go branch that currently
returns fmt.Errorf("remote path %s does not exist or is not accessible: %w",
u.Path, chErr) to wrap chErr with types.ErrInvalidPath (e.g., using
fmt.Errorf("%w: remote path %s does not exist or is not accessible: %v",
types.ErrInvalidPath, u.Path, chErr) or errors.Join/Wrap as appropriate) so
errors.Is(err, types.ErrInvalidPath) will succeed while preserving the original
chErr.
- Around line 66-68: The FTP dial should default to port 21 when the URL omits
one: before calling ftp.Dial in the function where conn is created (the block
that currently calls ftp.Dial(u.Host, ...)), check u.Host for an explicit port
and, if missing, build a host:port using net.JoinHostPort(u.Host, "21"); then
pass that resulting host:port string to ftp.Dial. Also add an import for net.
This ensures ftp.Dial receives host:port (hostname:21) when the URL has no port.

In `@internal/client/sftp/auth.go`:
- Around line 64-75: When privateKey is empty, don't probe default keys in a way
that can short-circuit explicit password/agent auth: only attempt
findDefaultKeyPath() and parseKeyAtPath(...) when no other auth methods have
been appended (i.e., methods is empty and no explicit password/agent is
configured). Move or guard the default-key discovery in the auth assembly so
that if a.keyPass (password) or agent-based auth is present you skip default
lookup; if you do probe and parseKeyAtPath fails, treat it as a non-fatal,
best-effort failure (do not return the error) so password/agent auth can still
be used. Ensure you still append ssh.PublicKeys(signer) when a privateKey
(explicit) is provided and preserve returning a parse error for an explicitly
specified key.

In `@internal/client/sftp/sftp.go`:
- Around line 165-169: The resolvePath method currently returns an empty string
when remotePath is empty even if c.basePath is configured, causing
Remove/RemoveDir to act on session cwd and UploadFile to try creating an empty
path; update resolvePath (method on Client) so that if remotePath == "" and
c.basePath != "" it returns c.basePath, otherwise fall back to returning
remotePath (and still return remotePath when c.basePath == ""); adjust callers
Remove, RemoveDir, and UploadFile to rely on the corrected resolvePath behavior
or validate/reject empty remotePath before destructive operations if desired.

In `@README.md`:
- Around line 157-179: Replace examples that embed secrets in the --dest URL
(the "FTP", "SFTP (password)" examples and any other occurrences) with versions
that avoid inline passwords/passphrases and instead demonstrate safer options
(SSH agent or key-based auth) and/or use placeholder tokens like
<PASSWORD_REDACTED>; add a short warning note above the examples advising not to
put secrets on the command line because they can be exposed via shell history
and process lists, and update the "SFTP (SSH key)" and "SFTP (SSH agent)"
examples to be the primary shown patterns (referencing the sftp-sync --dest
examples and query params ?key= and ?agent=true for exact placements).

---

Nitpick comments:
In `@internal/client/sftp/sftp_test.go`:
- Around line 8-47: The test currently only exercises net/url.Parse; update it
to call the project parsing helpers so it verifies our behavior: replace the
direct url.Parse usage in TestURLPathExtraction with calls to ParseConfigFromURL
(and where relevant ParseAuthFromURL) and/or instantiate a Client (to validate
its base-path handling) using the same rawURL inputs, then assert the returned
config.Path (or client.BasePath) and auth fields equal the expected wantPath and
expected auth values; keep the existing table of cases but assert against
ParseConfigFromURL/ParseAuthFromURL/Client behavior instead of u.Path so
regressions in our parsing and base-path logic are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74345cde-7d11-4c5d-a3ca-2f8e73a9a402

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef1868 and 36ef97c.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • .golangci.yml
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/ftp/ftp_test.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/config.go
  • internal/client/sftp/sftp.go
  • internal/client/sftp/sftp_test.go
  • internal/client/types/errors.go
  • main.go
✅ Files skipped from review due to trivial changes (1)
  • main.go

Comment thread go.mod
Comment thread internal/client/ftp/ftp.go Outdated
Comment thread internal/client/ftp/ftp.go Outdated
Comment thread internal/client/sftp/auth.go
Comment thread README.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/client/sftp/auth.go (1)

64-75: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

This guard disables key-based auth entirely.

After keyPath is populated above, this branch only runs when it is still empty, so parseKeyAtPath is called with "" while explicit ?key=... and discovered default keys are skipped. Key-only SFTP URLs will therefore never append ssh.PublicKeys(...).

🐛 Minimal fix
 keyPath := a.privateKey
-if keyPath == "" {
+if keyPath == "" && len(methods) == 0 && a.password == "" {
 	keyPath = findDefaultKeyPath()
 }
 
-if keyPath == "" && len(methods) == 0 && a.password == "" {
+if keyPath != "" {
 	signer, keyErr := parseKeyAtPath(keyPath, a.keyPass)
 	if keyErr != nil {
 		return nil, keyErr
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/auth.go` around lines 64 - 75, The current guard
incorrectly checks keyPath == "" so parseKeyAtPath is only attempted when no key
exists; change the condition to attempt key parsing when a key path is present
(whether from a.privateKey or findDefaultKeyPath()) by replacing the keyPath
empty check with a non-empty check so that when keyPath != "" and len(methods)
== 0 and a.password == "" you call parseKeyAtPath(keyPath, a.keyPass) and append
ssh.PublicKeys(signer); refer to the keyPath variable, findDefaultKeyPath(),
parseKeyAtPath(), methods slice, and a.password/a.privateKey to locate and fix
the logic.
♻️ Duplicate comments (1)
README.md (1)

194-199: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid normalizing inline secret usage in URL examples.

Even with the security note, examples still show inline password/passphrase patterns (username:password, key_pass=mysecret). Prefer redacted placeholders (e.g., <PASSWORD_REDACTED>, <PASSPHRASE_REDACTED>) and keep key/agent examples as primary.

Also applies to: 205-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 194 - 199, Update the README `--dest` URL examples to
avoid inline secrets by replacing `username:password` and
`key_pass=<passphrase>` with redacted placeholders (e.g., `<PASSWORD_REDACTED>`,
`<PASSPHRASE_REDACTED>`), keep the SFTP SSH key and agent examples as the
preferred patterns, and apply the same redactions to the other set of examples
around lines 205-209; locate the `--dest` examples and adjust the URL strings
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/ftp/ftp.go`:
- Around line 67-70: Reject empty FTP host before building the dial address: in
the code block around host := u.Host (function handling FTP URIs in
internal/client/ftp/ftp.go) add a guard that checks if u.Hostname() == "" and
immediately return types.ErrInvalidParams (rather than proceeding to
net.JoinHostPort or ftp.Dial); keep the existing logic that defaults port to
"21" only after this hostname validation so ftp.Dial never receives ":21" or
":2121" with an empty host.

In `@internal/client/sftp/sftp.go`:
- Around line 165-172: The resolvePath function can allow escaping basePath via
parent segments; change resolvePath to first clean remotePath (remove leading
"/" so it stays relative), join with c.basePath using path.Join, then verify the
resulting path does not escape by ensuring the joined result equals c.basePath
or has the c.basePath + "/" prefix (use strings.HasPrefix on the joined value);
if the check fails, return c.basePath (or otherwise clamp to basePath) so any
../ or absolute inputs cannot traverse outside the configured basePath.
Reference: Client.resolvePath and Client.basePath.

In `@README.md`:
- Around line 108-110: Replace the FTP-only prerequisite text "Access to an FTP
server with valid credentials" in the README (the prerequisite list around the
Go version entry) with the updated string "Access to an FTP or SFTP server with
valid credentials." to reflect support for both protocols.

---

Outside diff comments:
In `@internal/client/sftp/auth.go`:
- Around line 64-75: The current guard incorrectly checks keyPath == "" so
parseKeyAtPath is only attempted when no key exists; change the condition to
attempt key parsing when a key path is present (whether from a.privateKey or
findDefaultKeyPath()) by replacing the keyPath empty check with a non-empty
check so that when keyPath != "" and len(methods) == 0 and a.password == "" you
call parseKeyAtPath(keyPath, a.keyPass) and append ssh.PublicKeys(signer); refer
to the keyPath variable, findDefaultKeyPath(), parseKeyAtPath(), methods slice,
and a.password/a.privateKey to locate and fix the logic.

---

Duplicate comments:
In `@README.md`:
- Around line 194-199: Update the README `--dest` URL examples to avoid inline
secrets by replacing `username:password` and `key_pass=<passphrase>` with
redacted placeholders (e.g., `<PASSWORD_REDACTED>`, `<PASSPHRASE_REDACTED>`),
keep the SFTP SSH key and agent examples as the preferred patterns, and apply
the same redactions to the other set of examples around lines 205-209; locate
the `--dest` examples and adjust the URL strings accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb5304a8-c65d-4185-a7e3-d4902fe8ce1c

📥 Commits

Reviewing files that changed from the base of the PR and between 36ef97c and 102bcf9.

📒 Files selected for processing (4)
  • README.md
  • internal/client/ftp/ftp.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/sftp.go

Comment thread internal/client/ftp/ftp.go
Comment thread internal/client/sftp/sftp.go
Comment thread README.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/client/sftp/auth.go (1)

40-42: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Potential nil pointer dereference if u.User is nil.

ParseAuthFromURL is exported and could be called directly with a URL where User is nil, causing a panic at u.User.Password(). While ParseConfigFromURL validates this upstream, adding a defensive check improves robustness.

🛡️ Proposed fix
 func ParseAuthFromURL(u *url.URL) AuthConfig {
 	cfg := AuthConfig{
 		password:   "",
 		privateKey: "",
 		keyPass:    "",
 		useAgent:   false,
 	}
 
-	if pw, ok := u.User.Password(); ok {
+	if u.User != nil {
+		if pw, ok := u.User.Password(); ok {
+			cfg.password = pw
+		}
-		cfg.password = pw
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/auth.go` around lines 40 - 42, ParseAuthFromURL may
dereference u.User and panic; add a nil check before calling u.User.Password().
Wrap the existing password extraction in a guard like "if u.User != nil { if pw,
ok := u.User.Password(); ok { cfg.password = pw } }" (and similarly guard any
other uses of u.User) so ParseAuthFromURL handles URLs with no user info without
panicking.
🧹 Nitpick comments (1)
internal/client/sftp/auth.go (1)

22-22: 💤 Low value

Unused error variable ErrNoSignersFound.

This error is defined but never used in the current implementation. Either remove it or use it in buildAgentSigners when no signers are returned (currently returns an empty slice without error).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/auth.go` at line 22, The declared ErrNoSignersFound is
unused; update buildAgentSigners to return ErrNoSignersFound when it ends up
with zero signers (adjust its signature to return an error and propagate that
error to callers), or if the design prefers no-change to control flow, remove
the ErrNoSignersFound declaration entirely; reference buildAgentSigners and
ErrNoSignersFound when making the change so callers and error handling are
updated consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/client/sftp/auth.go`:
- Around line 40-42: ParseAuthFromURL may dereference u.User and panic; add a
nil check before calling u.User.Password(). Wrap the existing password
extraction in a guard like "if u.User != nil { if pw, ok := u.User.Password();
ok { cfg.password = pw } }" (and similarly guard any other uses of u.User) so
ParseAuthFromURL handles URLs with no user info without panicking.

---

Nitpick comments:
In `@internal/client/sftp/auth.go`:
- Line 22: The declared ErrNoSignersFound is unused; update buildAgentSigners to
return ErrNoSignersFound when it ends up with zero signers (adjust its signature
to return an error and propagate that error to callers), or if the design
prefers no-change to control flow, remove the ErrNoSignersFound declaration
entirely; reference buildAgentSigners and ErrNoSignersFound when making the
change so callers and error handling are updated consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6eef276b-789f-43b3-8f91-132eebdf689d

📥 Commits

Reviewing files that changed from the base of the PR and between 102bcf9 and accc175.

📒 Files selected for processing (2)
  • README.md
  • internal/client/sftp/auth.go
✅ Files skipped from review due to trivial changes (1)
  • README.md

@capcom6 capcom6 force-pushed the sftp/add-support branch from 1507873 to 2b5a508 Compare May 26, 2026 07:16
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/sftp/sftp.go`:
- Around line 99-104: The error returned when client.Stat(u.Path) fails should
be wrapped with types.ErrInvalidPath so callers can reliably detect invalid-path
errors; update the return in the client.Stat error branch (the block using
client.Stat, client.Close(), conn.Close()) to return an error that includes
types.ErrInvalidPath via %w and also preserves the original statErr details
(e.g. fmt.Errorf("%w: remote path %s does not exist: %v", types.ErrInvalidPath,
u.Path, statErr)), referencing the client.Stat/u.Path error branch in sftp.go.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3b5d4ca-eb18-43c5-8ccf-103a44b921d8

📥 Commits

Reviewing files that changed from the base of the PR and between 1507873 and 2b5a508.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • .golangci.yml
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/client/client.go
  • internal/client/ftp/ftp.go
  • internal/client/ftp/ftp_test.go
  • internal/client/sftp/auth.go
  • internal/client/sftp/config.go
  • internal/client/sftp/sftp.go
  • internal/client/types/errors.go
  • main.go
✅ Files skipped from review due to trivial changes (4)
  • internal/cli/commands/sync/sync.go
  • main.go
  • README.md
  • .golangci.yml

Comment thread internal/client/sftp/sftp.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/client/sftp/sftp.go (1)

98-103: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only map actual not-found failures to ErrInvalidPath.

Line 103 now treats every client.Stat(u.Path) error as “does not exist”. That misclassifies permission or transport failures as bad input and gives callers a false errors.Is(err, types.ErrInvalidPath) signal. Wrap types.ErrInvalidPath only for genuine missing-path cases; otherwise return the original stat failure.

Suggested patch
 	if u.Path != "" && u.Path != "/" {
 		info, statErr := client.Stat(u.Path)
 		if statErr != nil {
 			_ = client.Close()
 			_ = conn.Close()
-			return fmt.Errorf("%w: remote path %s does not exist: %w", types.ErrInvalidPath, u.Path, statErr)
+			if isIgnorableError(statErr) {
+				return fmt.Errorf("%w: remote path %s does not exist: %w", types.ErrInvalidPath, u.Path, statErr)
+			}
+			return fmt.Errorf("can't stat remote path %s: %w", u.Path, statErr)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/sftp/sftp.go` around lines 98 - 103, The code currently maps
any client.Stat(u.Path) error to types.ErrInvalidPath; instead, detect genuine
"not found" errors (e.g. via os.IsNotExist(statErr) or errors.Is(statErr,
os.ErrNotExist)) and only then wrap and return types.ErrInvalidPath; for all
other statErr values return the original statErr (after closing client and conn
as already done). Update the branch around client.Stat(u.Path) to check the
nature of statErr before wrapping with types.ErrInvalidPath so callers only get
ErrInvalidPath for actual missing-path cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/client/sftp/sftp.go`:
- Around line 98-103: The code currently maps any client.Stat(u.Path) error to
types.ErrInvalidPath; instead, detect genuine "not found" errors (e.g. via
os.IsNotExist(statErr) or errors.Is(statErr, os.ErrNotExist)) and only then wrap
and return types.ErrInvalidPath; for all other statErr values return the
original statErr (after closing client and conn as already done). Update the
branch around client.Stat(u.Path) to check the nature of statErr before wrapping
with types.ErrInvalidPath so callers only get ErrInvalidPath for actual
missing-path cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4fc0b1df-806e-4b13-b187-183d3faf3a99

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5a508 and d50cab7.

📒 Files selected for processing (1)
  • internal/client/sftp/sftp.go

@capcom6 capcom6 force-pushed the sftp/add-support branch from d50cab7 to 09f9be6 Compare May 27, 2026 00:33
@capcom6 capcom6 marked this pull request as ready for review May 27, 2026 07:21
@capcom6 capcom6 added the ready label May 27, 2026
@capcom6 capcom6 merged commit 566ba70 into master May 28, 2026
8 checks passed
@capcom6 capcom6 deleted the sftp/add-support branch May 28, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants