Skip to content

feat: enhance background folder watcher and improve UX/stability#22

Open
amemya wants to merge 4 commits into
mainfrom
feat/background-folder-watcher
Open

feat: enhance background folder watcher and improve UX/stability#22
amemya wants to merge 4 commits into
mainfrom
feat/background-folder-watcher

Conversation

@amemya
Copy link
Copy Markdown
Owner

@amemya amemya commented May 22, 2026

  • Refactor OnBeforeClose behavior to restore native macOS window close functionality
  • Fix fsnotify watcher goroutine leaks by properly syncing Close() and app shutdown hooks
  • Fix infinite loop in SaveSettings by validating bidirectional Watch/Export folder hierarchy
  • Optimize file reading in processBackgroundFile and OpenImage to eliminate redundant disk I/O
  • Debounce background processing effectively by checking for file size stabilization
  • Improve macOS UX by extracting the Application Menu to buildMenu and supporting Escape to close preferences
  • Add osascript native macOS notifications for background save success, with AppleScript injection defense
  • Migrate Watch/Export folder configuration in React to use Wails native directory selection dialogs
  • Resolve React strict-mode issues with EventsOn/EventsOff and fix stale closures in useEffect dependencies
  • Add proper error handling for settings JSON marshalling and unmarshalling
  • Remove unused icon.go and dead React states

(Resolved #1 )

- Refactor OnBeforeClose behavior to restore native macOS window close functionality
- Fix fsnotify watcher goroutine leaks by properly syncing Close() and app shutdown hooks
- Fix infinite loop in SaveSettings by validating bidirectional Watch/Export folder hierarchy
- Optimize file reading in processBackgroundFile and OpenImage to eliminate redundant disk I/O
- Debounce background processing effectively by checking for file size stabilization
- Improve macOS UX by extracting the Application Menu to buildMenu and supporting Escape to close preferences
- Add osascript native macOS notifications for background save success, with AppleScript injection defense
- Migrate Watch/Export folder configuration in React to use Wails native directory selection dialogs
- Resolve React strict-mode issues with EventsOn/EventsOff and fix stale closures in useEffect dependencies
- Add proper error handling for settings JSON marshalling and unmarshalling
- Remove unused icon.go and dead React states
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

新機能

  • フォルダ監視機能で対象フォルダの画像を自動検出・処理
  • 設定値の保存・復元機能を実装
  • 自動保存機能(エクスポートフォルダへの自動出力)
  • macOS上での保存完了通知

改善

  • EXIF データ処理の精度向上
  • 広色域(P3)カラー対応

Walkthrough

設定の永続化(Watch/Export 等)、fsnotify によるフォルダ監視、背景処理→自動保存トークン経由の保存パイプ、フロントエンドのキャンバス描画と Preferences モーダル、OS別メニュー/通知を追加します。

Changes

設定、ファイルウォッチング、自動保存パイプライン

Layer / File(s) Summary
設定データモデルと永続化
settings.go, frontend/wailsjs/go/models.ts, frontend/wailsjs/go/main/App.d.ts, frontend/wailsjs/go/main/App.js, go.mod, .gitignore
Settings 構造体を定義し、ディスクへの読み書き(loadSettings/saveSettings)、パス正規化、GetSettings/SaveSettings 公開 API を実装。frontend 側の Settings モデルと生成済み API 型/ラッパーを追加し、fsnotify を go.mod に追加。
ファイル監視とライフサイクル
watcher.go, app.go
fsnotify を用いたフォルダ監視を実装し、イベントのデバウンスとファイルサイズ安定待ちの後に非同期処理を実行。startup/shutdown は WatchFolder 設定に応じて watcher を開始/停止する。
画像処理と自動保存 API
app.go, handler.go
OpenImageprocessImageFile/doOpenImage に分割。SaveAutoImage を追加して ExportFolder 下の保存先検証と保存トークン発行を行う。SelectWatchFolder/SelectExportFolder を追加。handler.go に macOS の保存完了通知(osascript)を非同期で発行する処理を追加。
キャンバス描画と process_file ハンドラ
frontend/src/App.tsx, frontend/src/App.css
renderImageToCanvas ヘルパを導入して描画ロジックを集約。process_file ハンドラはオフスクリーン Canvas で描画 → PNG/JPEG へ変換 → SaveAutoImage でトークン取得 → /api/save に ArrayBuffer を POST するフローに整理。Preferences モーダル用の CSS を追加し、トースト/デバウンス/アンマウント後片付けを実装。
設定 UI と状態管理、Preferences モーダル
frontend/src/App.tsx
マウント時に GetSettings() で設定を取得、open_settings イベントでモーダル表示、Escape キーで閉じる。設定変更は 500ms デバウンスで SaveSettings を呼び、成功/失敗をトーストで通知。モーダルは Watch/Export フォルダの選択/クリアを提供。
アプリケーションメニューとメイン統合
main.go
buildMenu を追加して OS ごとにメニューを構築(macOS は Preferences を含む)。生成した AppMenu を wails.Run に渡し、OnShutdown を app.shutdown に設定。

Sequence Diagram

sequenceDiagram
  participant User
  participant Frontend
  participant App
  participant Watcher
  participant Settings
  participant Canvas
  participant SaveHandler

  User->>Frontend: open settings / select folders
  Frontend->>App: GetSettings()
  App->>Settings: read currentSettings
  Settings-->>Frontend: Settings
  Frontend->>App: SaveSettings(updated)
  App->>Settings: validate & persist
  App->>Watcher: updateWatcher(newFolder)

  Watcher->>Watcher: detect new image file
  Watcher->>App: processBackgroundFile(path)
  App->>App: processImageFile -> doOpenImage
  App->>Frontend: emit process_file(result, exportFolder)
  Frontend->>Canvas: renderImageToCanvas(result.imageURL)
  Canvas-->>Frontend: toBlob(ArrayBuffer)
  Frontend->>App: SaveAutoImage(isPng, savePath)
  App->>SaveHandler: handler.prepareSave -> token
  Frontend->>SaveHandler: POST /api/save with ArrayBuffer + token
  SaveHandler-->>Frontend: success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • amemya/ExifFrame#15: /api/save トークン・保存フローと handler.prepareSave に関するコード接点があります。
  • amemya/ExifFrame#20: フレーム描画ロジック(アスペクト比・向き・配置)で重複する変更領域があります。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: enhancing a background folder watcher and improving UX/stability, which aligns with the core objective of implementing continuous folder monitoring.
Description check ✅ Passed The description is directly related to the changeset, detailing multiple implementation aspects of folder watching, settings management, file processing, UI improvements, and stability fixes.
Linked Issues check ✅ Passed The PR successfully implements the core requirements from issue #1: continuous folder monitoring (watcher.go), automatic frame addition to new images (processBackgroundFile, SaveAutoImage), and configurable settings for Watch/Export folders with UI integration.
Out of Scope Changes check ✅ Passed All changes are within scope. The PR includes folder watching (watcher.go, settings.go), background image processing (app.go), React UI enhancements (App.tsx, App.css), settings persistence (settings.go), macOS notifications (handler.go, main.go), and necessary dependencies (go.mod, auto-generated bindings).

✏️ 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 feat/background-folder-watcher

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
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances ExifFrame’s background auto-processing pipeline and preferences UX by adding a folder watcher, persisted settings, automated export saving, and macOS-centric UI integrations (menu + notifications) within the Wails desktop app.

Changes:

  • Add fsnotify-based background watch folder processing that emits process_file events to the frontend.
  • Introduce persisted settings (watch/export folders + rendering preferences) with frontend Preferences modal using native directory pickers.
  • Add macOS application menu integration and native save notifications via osascript, plus refactors to reduce redundant file I/O.

Reviewed changes

Copilot reviewed 10 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
watcher.go Adds/controls fsnotify watcher and emits background processing events to the frontend
settings.go Implements settings persistence + validation and triggers watcher restart on changes
app.go Refactors image open path to reuse processing; adds auto-save and directory selection APIs
main.go Adds cross-platform menu building with macOS Preferences integration and shutdown hook
handler.go Adds macOS notification on save completion
go.mod / go.sum Adds fsnotify dependency
frontend/src/App.tsx Adds Preferences modal, settings load/save, and background auto-save pipeline
frontend/src/App.css Styles the Preferences modal
frontend/wailsjs/runtime/runtime.js Adds runtime JS bindings used by the React app
frontend/wailsjs/runtime/runtime.d.ts Adds runtime TypeScript definitions (note: must match JS exports)
frontend/wailsjs/runtime/package.json Packages runtime bindings metadata
frontend/wailsjs/go/models.ts Adds Settings model in generated bindings
frontend/wailsjs/go/main/App.js Updates generated App bindings (new backend methods)
frontend/wailsjs/go/main/App.d.ts Updates generated App typings (new backend methods)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/App.tsx Outdated
Comment on lines +295 to +299
if (!isEventsRegistered.current) {
EventsOn("open_settings", () => {
console.log("open_settings event received");
setShowSettings(true);
});
Comment thread settings.go
Comment on lines +3 to +10
import (
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
"sync"
)
Comment thread settings.go
Comment on lines +90 to +103
// Validate folder hierarchy to prevent infinite loop (Task 9)
if s.WatchFolder != "" && s.ExportFolder != "" {
watchClean := filepath.Clean(s.WatchFolder)
exportClean := filepath.Clean(s.ExportFolder)
if watchClean == exportClean {
return "Error: Export folder cannot be the same as the Watch folder."
}
if strings.HasPrefix(exportClean, watchClean+string(filepath.Separator)) {
return "Error: Export folder cannot be a subdirectory of the Watch folder."
}
if strings.HasPrefix(watchClean, exportClean+string(filepath.Separator)) {
return "Error: Watch folder cannot be a subdirectory of the Export folder."
}
}
Comment thread handler.go Outdated
Comment on lines +251 to +259
// Show a native notification on macOS
if runtime.GOOS == "darwin" {
fileName := filepath.Base(savePath)
// Prevent AppleScript injection
fileName = strings.ReplaceAll(fileName, `\`, `\\`)
fileName = strings.ReplaceAll(fileName, `"`, `\"`)
msg := "Saved " + fileName
exec.Command("osascript", "-e", `display notification "`+msg+`" with title "ExifFrame"`).Run()
}
Comment thread settings.go
Comment on lines +29 to +34
configDir, err := os.UserConfigDir()
if err == nil {
appDir := filepath.Join(configDir, "ExifFrame")
os.MkdirAll(appDir, 0755)
settingsFile = filepath.Join(appDir, "settings.json")
}
Copy link
Copy Markdown
Contributor

@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: 9

🤖 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 `@app.go`:
- Around line 357-360: SelectWatchFolder and SelectExportFolder currently
discard the error returned by runtime.OpenDirectoryDialog, conflating
user-cancel with real failures; change both functions (the calls to
runtime.OpenDirectoryDialog) to capture the returned error, check if err != nil
and log it (or return it) instead of ignoring it — add a log import to app.go if
necessary and use process-appropriate logging in the error branch so real errors
are preserved and distinguishable from an empty path caused by cancelation.
- Around line 338-342: The current check using filepath.Clean and
strings.HasPrefix can be bypassed by symlinks; update the validation in the
SaveAutoImage/save path logic to resolve symlinks and compare real paths: call
filepath.EvalSymlinks on both savePath and exportFolder (the variables used
where you build cleanSave/cleanExport), then use filepath.Rel to compute the
relative path and ensure it does not begin with ".." (meaning the save is
outside the export folder); return the same SaveResult error when the resolved
relative path escapes the export folder. Ensure you handle and propagate any
EvalSymlinks errors (treat them as validation failures) and reference the
existing SaveResult return path in the function.

In `@frontend/src/App.css`:
- Around line 388-395: .modal-content uses a fixed width (width: 400px) causing
clipping on small viewports; update the .modal-content rule to use a responsive
width and constrained dimensions (e.g., width: 90% with max-width: 400px and
max-height: 80vh) and enable internal scrolling (overflow: auto) so content
doesn’t overflow on narrow or short windows while preserving the 400px cap.

In `@frontend/src/App.tsx`:
- Around line 185-197: The background-save callback handling the
offscreenCanvas.toBlob result treats any fetch response as success; in the
offscreenCanvas.toBlob callback (the async function that calls SaveAutoImage and
fetch with resultSave.saveToken and savePath) check the fetch response status by
awaiting the fetch call into a variable and asserting response.ok; if not ok,
log or throw an error (include response.status/text) and avoid printing
"Background save complete" on failure; ensure this error path is handled
consistently with the surrounding try/catch so failures are surfaced rather than
reported as success.
- Around line 278-293: The effect that calls GetSettings() only clears the
initial-load guard when the promise resolves, so a failed GetSettings() leaves
isInitialLoad.current true and prevents SaveSettings from ever running; update
the useEffect that calls GetSettings() to ensure the setTimeout that sets
isInitialLoad.current = false runs regardless of success or failure by using a
try/catch/finally (or .catch/.finally) around GetSettings() and moving the
setTimeout into the finally block, keeping the existing setXYZ calls
(setWatchFolder, setExportFolder, setAspectRatioPreset, setCustomRatioW,
setCustomRatioH, setOrientation, setAlignment) in the success path and
preserving the 100ms delay and timeout semantics.

In `@handler.go`:
- Around line 251-259: The osascript call in handleSave blocks the /api/save
response because exec.Command(...).Run() is executed synchronously and its error
is ignored; change this to create a context with a short timeout and call
exec.CommandContext (or otherwise move the call to a goroutine/run it after
w.WriteHeader) so the HTTP response is sent immediately, and capture and log any
error from the command instead of ignoring it; specifically update the block
that builds the message and calls exec.Command("osascript", ...).Run() to use
context.WithTimeout + exec.CommandContext (or spawn a goroutine) and process the
returned error via your logger (e.g., processLogger or the request-scoped
logger) rather than blocking the handler.

In `@main.go`:
- Line 16: The build fails because the //go:embed all:frontend/dist directive in
main.go references files that don't exist; either ensure the frontend/dist
artifact is produced before the Go build (add a CI step that runs the frontend
build and outputs frontend/dist), or change the embedding strategy to target
only existing files (e.g., embed specific patterns like //go:embed
frontend/dist/* or remove/embed conditionally and fallback to runtime FS) so
compilation doesn't fail when frontend/dist is absent; update main.go
accordingly to use the chosen approach.

In `@settings.go`:
- Around line 64-79: Change saveSettings() to return an error instead of
swallowing failures: have saveSettings check settingsFile, marshal
currentSettings under settingsMu, attempt os.WriteFile and return any
encountered error (including marshal errors). Update the caller SaveSettings()
to call the new saveSettings() and use its returned error to set the response
string/return value (e.g., return success only when err == nil, otherwise return
an error message). Refer to saveSettings(), SaveSettings(), settingsMu,
currentSettings and settingsFile when making the changes.
- Around line 47-60: loadSettings currently unmarshals into a zero-value temp
which overwrites defaults when loading older settings; instead initialize temp
with the existing defaults/currentSettings (e.g., temp := currentSettings or a
default Settings value) before calling json.Unmarshal so only fields present in
the file replace defaults, then lock settingsMu and assign currentSettings =
temp; keep the error handling and file-read logic the same and still return on
unmarshal error.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: fecca53a-7fb7-49cd-8796-60ff316f3142

📥 Commits

Reviewing files that changed from the base of the PR and between 2f94261 and 3af4eb8.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • app.go
  • frontend/src/App.css
  • frontend/src/App.tsx
  • frontend/wailsjs/go/main/App.d.ts
  • frontend/wailsjs/go/main/App.js
  • frontend/wailsjs/go/models.ts
  • frontend/wailsjs/runtime/package.json
  • frontend/wailsjs/runtime/runtime.d.ts
  • frontend/wailsjs/runtime/runtime.js
  • go.mod
  • handler.go
  • main.go
  • settings.go
  • watcher.go

Comment thread app.go
Comment thread app.go Outdated
Comment thread frontend/src/App.css
Comment thread frontend/src/App.tsx
Comment thread frontend/src/App.tsx
Comment thread handler.go Outdated
Comment thread main.go
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)

//go:embed all:frontend/dist
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

go:embed の対象欠落でビルドが失敗します。

Line [16] の all:frontend/dist は対象が存在しないとコンパイルエラーになり、提示された静的解析でも no matching files found が出ています。CI で Go ビルド前に frontend/dist を必ず生成するか、埋め込み戦略を調整してください。

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 16-16: pattern all:frontend/dist: no matching files found

(typecheck)

🤖 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 `@main.go` at line 16, The build fails because the //go:embed all:frontend/dist
directive in main.go references files that don't exist; either ensure the
frontend/dist artifact is produced before the Go build (add a CI step that runs
the frontend build and outputs frontend/dist), or change the embedding strategy
to target only existing files (e.g., embed specific patterns like //go:embed
frontend/dist/* or remove/embed conditionally and fallback to runtime FS) so
compilation doesn't fail when frontend/dist is absent; update main.go
accordingly to use the chosen approach.

Comment thread settings.go
Comment thread settings.go Outdated
… optimize UI responsiveness and notifications
Copy link
Copy Markdown
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
settings.go (1)

124-132: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

保存失敗時に live settings だけ更新されています。

Line 126 で currentSettings を先に差し替えているため、saveSettings() が失敗しても GetSettings() は新しい値を返し、updateWatcher() だけは未実行の中途半端な状態になります。永続化成功後に反映するか、失敗時にロールバックしてください。

🤖 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 `@settings.go` around lines 124 - 132, The code updates currentSettings before
persisting, causing GetSettings() to return new values even if saveSettings()
fails and leaving updateWatcher() unrun; fix by persisting first or rolling back
on error: either call saveSettings() using the new settings (s) while still
holding settingsMu and only assign currentSettings = s and call updateWatcher()
after saveSettings() returns nil, or keep the current assignment but on
saveSettings() error restore currentSettings = oldWatch (or original settings
snapshot) and ensure updateWatcher() is invoked/rolled back accordingly; use the
existing symbols settingsMu, currentSettings, saveSettings(), oldWatch,
GetSettings(), and updateWatcher() to locate and implement the change.
🤖 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 `@app.go`:
- Around line 344-348: The current EvalSymlinks call on saveDir rejects new
subfolders because EvalSymlinks fails if the parent doesn't exist; change the
validation in the save path handling so you locate the nearest existing ancestor
of saveDir (walk up using filepath.Dir until you find an existing directory via
os.Stat) and call filepath.EvalSymlinks only on that existing ancestor (store
result in realSaveDirBase), then reconstruct/validate the intended final path
relative to that resolved base; do not call MkdirAll here—defer actual directory
creation to the save routine—and return SaveResult errors only for genuine
invalid resolved-base errors (keep references to savePath, saveDir,
realSaveDir/realSaveDirBase, filepath.EvalSymlinks, and MkdirAll to find the
affected code).

In `@settings.go`:
- Around line 111-120: The current checks use cleaned string paths which can be
bypassed by symlinks; resolve the actual filesystem paths for s.WatchFolder and
s.ExportFolder before comparing (e.g., call filepath.EvalSymlinks on each, then
filepath.Clean and pass those into normalizePathForCompare) so the comparisons
in the block using watchNorm/exportNorm detect real parent/child relationships
the same way SaveAutoImage does; update the code that sets watchNorm and
exportNorm to use the resolved real paths and handle EvalSymlinks errors
appropriately.

---

Outside diff comments:
In `@settings.go`:
- Around line 124-132: The code updates currentSettings before persisting,
causing GetSettings() to return new values even if saveSettings() fails and
leaving updateWatcher() unrun; fix by persisting first or rolling back on error:
either call saveSettings() using the new settings (s) while still holding
settingsMu and only assign currentSettings = s and call updateWatcher() after
saveSettings() returns nil, or keep the current assignment but on saveSettings()
error restore currentSettings = oldWatch (or original settings snapshot) and
ensure updateWatcher() is invoked/rolled back accordingly; use the existing
symbols settingsMu, currentSettings, saveSettings(), oldWatch, GetSettings(),
and updateWatcher() to locate and implement the change.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 17485f33-e433-4cc5-b6a1-93dd9ad1ee1c

📥 Commits

Reviewing files that changed from the base of the PR and between 3af4eb8 and a342ddd.

📒 Files selected for processing (6)
  • ExifFrame
  • app.go
  • frontend/src/App.css
  • frontend/src/App.tsx
  • handler.go
  • settings.go

Comment thread app.go Outdated
Comment thread settings.go Outdated
@amemya
Copy link
Copy Markdown
Owner Author

amemya commented May 23, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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

🤖 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 `@app.go`:
- Around line 366-368: 現在の脱出判定が strings.HasPrefix(rel, "..") だと "..archive"
のような合法的な子ディレクトリを誤検知するので、filepath.Rel の結果で拒否する条件を「rel がちょうど \"..\" である」か「rel が
\"..\" + パス区切り文字で始まる」場合に限定して下さい(例: rel == ".." || strings.HasPrefix(rel,
".."+string(filepath.Separator)))。該当箇所は rel を調べているブロック(rel, err :=
filepath.Rel(...) とその直後の if ブロック)を更新し、拒否時は従来どおり SaveResult{Error: ...}
を返すようにしてください。

In `@settings.go`:
- Around line 154-156: SaveSettings currently calls updateWatcher(s.WatchFolder)
after persisting without checking for errors, so failures in watcher.go (e.g.,
fsnotify.NewWatcher or watcher.Add) are ignored and the frontend still shows
"Settings saved"; change updateWatcher to return an error, have SaveSettings
call it and if it fails return that error (and revert s.WatchFolder to
oldSettings.WatchFolder or rollback the persisted WatchFolder), ensuring
failures in watcher initialization (fsnotify.NewWatcher / watcher.Add in
watcher.go) are propagated back to the caller and cause SaveSettings to fail.
- Around line 132-135: The current subdirectory checks using
strings.HasPrefix(exportReal, watchReal+string(filepath.Separator)) miss cases
when watchReal or exportReal is root ("/") because concatenating an extra
separator yields "//"; update the logic in settings.go around the
watchReal/exportReal checks to first ensure both paths are cleaned (use
filepath.Clean if not already) and add explicit root handling: treat watchReal
== string(filepath.Separator) as matching any exportReal that starts with
string(filepath.Separator), and likewise treat exportReal ==
string(filepath.Separator) as matching any watchReal that starts with
string(filepath.Separator); i.e., change the conditions around strings.HasPrefix
so they also check for equality to filepath.Separator before relying on the
HasPrefix(w+sep) pattern.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: a3ac8c61-b22d-49d7-b54d-43c60a294b00

📥 Commits

Reviewing files that changed from the base of the PR and between a342ddd and 343fd38.

📒 Files selected for processing (3)
  • .gitignore
  • app.go
  • settings.go

Comment thread app.go
Comment on lines +366 to +368
rel, err := filepath.Rel(realExport, realAncestor)
if err != nil || strings.HasPrefix(rel, "..") {
return SaveResult{Error: "Save path is outside of the allowed export folder"}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

.. で始まる合法な子ディレクトリも誤検知します。

savePath<export>/..archive/photo.jpg のようなケースだと、rel"..archive" になり、export 配下なのにここで拒否されます。脱出判定は .. 自体と ../... だけに絞ってください。

修正案
-	if err != nil || strings.HasPrefix(rel, "..") {
+	if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
 		return SaveResult{Error: "Save path is outside of the allowed export folder"}
 	}
🤖 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 `@app.go` around lines 366 - 368, 現在の脱出判定が strings.HasPrefix(rel, "..") だと
"..archive" のような合法的な子ディレクトリを誤検知するので、filepath.Rel の結果で拒否する条件を「rel がちょうど \"..\"
である」か「rel が \"..\" + パス区切り文字で始まる」場合に限定して下さい(例: rel == ".." ||
strings.HasPrefix(rel, ".."+string(filepath.Separator)))。該当箇所は rel
を調べているブロック(rel, err := filepath.Rel(...) とその直後の if ブロック)を更新し、拒否時は従来どおり
SaveResult{Error: ...} を返すようにしてください。

Comment thread settings.go
Comment on lines +132 to +135
if strings.HasPrefix(exportReal, watchReal+string(filepath.Separator)) {
return "Error: Export folder cannot be a subdirectory of the Watch folder."
}
if strings.HasPrefix(watchReal, exportReal+string(filepath.Separator)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ルートディレクトリ配下の判定が抜けます。

watchRealexportReal/ のとき、ここは "//" との前方一致になってしまうので、/tmp/out のような実際の子パスを検出できません。結果として、ルートを選ばれると今回の無限ループ防止を迂回できます。

修正案
+		isSubdir := func(base, target string) bool {
+			rel, err := filepath.Rel(base, target)
+			return err == nil &&
+				rel != "." &&
+				rel != ".." &&
+				!strings.HasPrefix(rel, ".."+string(filepath.Separator))
+		}
+
 		if watchReal == exportReal {
 			return "Error: Export folder cannot be the same as the Watch folder."
 		}
-		if strings.HasPrefix(exportReal, watchReal+string(filepath.Separator)) {
+		if isSubdir(watchReal, exportReal) {
 			return "Error: Export folder cannot be a subdirectory of the Watch folder."
 		}
-		if strings.HasPrefix(watchReal, exportReal+string(filepath.Separator)) {
+		if isSubdir(exportReal, watchReal) {
 			return "Error: Watch folder cannot be a subdirectory of the Export folder."
 		}
🤖 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 `@settings.go` around lines 132 - 135, The current subdirectory checks using
strings.HasPrefix(exportReal, watchReal+string(filepath.Separator)) miss cases
when watchReal or exportReal is root ("/") because concatenating an extra
separator yields "//"; update the logic in settings.go around the
watchReal/exportReal checks to first ensure both paths are cleaned (use
filepath.Clean if not already) and add explicit root handling: treat watchReal
== string(filepath.Separator) as matching any exportReal that starts with
string(filepath.Separator), and likewise treat exportReal ==
string(filepath.Separator) as matching any watchReal that starts with
string(filepath.Separator); i.e., change the conditions around strings.HasPrefix
so they also check for equality to filepath.Separator before relying on the
HasPrefix(w+sep) pattern.

Comment thread settings.go
Comment on lines +154 to +156
if oldSettings.WatchFolder != s.WatchFolder {
a.updateWatcher(s.WatchFolder)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

ウォッチャ再起動の失敗が保存結果に反映されません。

ここは保存成功後に updateWatcher() を呼ぶだけなので、watcher.go 側で fsnotify.NewWatcher / watcher.Add が失敗しても SaveSettings() は空文字を返し、フロントは「Settings saved」を表示します。監視開始に失敗したらエラーを返せるようにして、必要なら WatchFolder もロールバックしてください。

🤖 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 `@settings.go` around lines 154 - 156, SaveSettings currently calls
updateWatcher(s.WatchFolder) after persisting without checking for errors, so
failures in watcher.go (e.g., fsnotify.NewWatcher or watcher.Add) are ignored
and the frontend still shows "Settings saved"; change updateWatcher to return an
error, have SaveSettings call it and if it fails return that error (and revert
s.WatchFolder to oldSettings.WatchFolder or rollback the persisted WatchFolder),
ensuring failures in watcher initialization (fsnotify.NewWatcher / watcher.Add
in watcher.go) are propagated back to the caller and cause SaveSettings to fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

指定したフォルダの常時監視

2 participants