Skip to content

feat: add volume export functionality and UI integration#38

Merged
enegalan merged 3 commits into
mainfrom
24-feat-volumes-export
Jul 5, 2026
Merged

feat: add volume export functionality and UI integration#38
enegalan merged 3 commits into
mainfrom
24-feat-volumes-export

Conversation

@enegalan

@enegalan enegalan commented Jul 5, 2026

Copy link
Copy Markdown
Owner
  • Introduced a new Exports tab in the Volume Detail screen, allowing users to manage volume exports.
  • Implemented backend API endpoints for volume exports, including creation and download functionalities.
  • Added a Quick export flow for exporting volumes to local files, images, or registries.
  • Enhanced the UI with components for displaying export history and initiating exports.
  • Updated the changelog to reflect these new features and improvements.

Summary by CodeRabbit

  • New Features
    • Added volume export support, including quick exports to files, images, or registry destinations.
    • Added scheduled exports with editable timing, enabled/paused state, and export history.
    • Added download access for completed exports from the volume details screen.
  • Bug Fixes
    • Improved export naming and scheduling behavior with clearer defaults and unique timestamp-based names.
  • Documentation
    • Updated release notes, roadmap, and UI layout references to reflect the new export screens and workflow.

- Introduced a new **Exports** tab in the Volume Detail screen, allowing users to manage volume exports.
- Implemented backend API endpoints for volume exports, including creation and download functionalities.
- Added a **Quick export** flow for exporting volumes to local files, images, or registries.
- Enhanced the UI with components for displaying export history and initiating exports.
- Updated the changelog to reflect these new features and improvements.
@enegalan enegalan linked an issue Jul 5, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@enegalan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c9f22bc-7bab-45d3-abe8-3465a1307953

📥 Commits

Reviewing files that changed from the base of the PR and between 53add30 and a033f1f.

📒 Files selected for processing (20)
  • .cursor/rules/calf.mdc
  • CLAUDE.md
  • backend/internal/api/server.go
  • backend/internal/api/volume_export_scheduler.go
  • backend/internal/api/volume_export_schedules.go
  • backend/internal/api/volume_exports.go
  • backend/internal/api/volumes.go
  • backend/internal/runtime/lima.go
  • backend/internal/runtime/mock.go
  • backend/internal/runtime/native.go
  • backend/internal/runtime/runtime.go
  • backend/internal/runtime/volume_export.go
  • backend/internal/volumeexport/name_pattern.go
  • backend/internal/volumeexport/name_pattern_test.go
  • backend/internal/volumeexport/schedule.go
  • backend/internal/volumeexport/store.go
  • backend/test/api/api_test.go
  • ui/lib/api/client.dart
  • ui/lib/screens/volume_detail_screen.dart
  • ui/test/widget_test.dart
📝 Walkthrough

Walkthrough

This PR adds volume export and scheduled export capabilities to Calf. Backend changes introduce a runtime export engine, JSON-backed export/schedule stores, HTTP handlers, and a scheduler daemon. UI changes add quick-export and schedule-export screens, an Exports tab, client models/methods, and file-picker plugin wiring across platforms.

Changes

Volume Export & Scheduling

Layer / File(s) Summary
Runtime export execution engine
backend/internal/runtime/volume_export.go, runtime.go, lima.go, native.go, mock.go
Adds VolumeExportOptions/RunVolumeExport supporting local file/image/registry exports and implements ExportVolume across Lima, Native, and Mock runtimes.
Export naming and schedule timing logic
backend/internal/volumeexport/name_pattern*.go, schedule_timing*.go
Adds filename/image-ref pattern expansion and schedule normalization/validation/next-run computation with unit tests.
Export and schedule persistence stores
backend/internal/volumeexport/store.go, schedule.go
Adds JSON-backed Store and ScheduleStore types for CRUD on export metadata and schedules.
API handlers, scheduler daemon, and routing
backend/internal/api/server.go, volume_export_runner.go, volume_export_scheduler.go, volume_export_schedules.go, volume_exports.go, volumes.go, backend/test/api/api_test.go
Adds HTTP endpoints for exports/schedules, executeVolumeExport, a background exportScheduler wired into Server lifecycle, routing updates, and API tests.
UI API client models and methods
ui/lib/api/client.dart, ui/lib/export_name_pattern.dart
Adds VolumeExportItem/VolumeExportScheduleItem models and CalfClient/ApiClient methods for exports/schedules plus name-pattern helpers.
Quick export and schedule export screens
ui/lib/screens/volume_quick_export_screen.dart, volume_schedule_export_screen.dart
Adds VolumeQuickExportView and VolumeScheduleExportView with destination selection, timing configuration, and supporting widgets.
Volume detail screen integration and tests
ui/lib/screens/volume_detail_screen.dart, ui/test/widget_test.dart
Adds an Exports tab with export history, schedule toggling, download flow, and updates test fakes.
File picker plugin wiring and version bump
ui/linux/..., ui/macos/..., ui/windows/..., ui/pubspec.yaml
Registers file_selector plugin per platform, adds macOS entitlements, and bumps app version.
Documentation, changelog, and version updates
.cursor/rules/calf.mdc, CLAUDE.md, ROADMAP.md, CHANGELOG.md, backend/version/version.go
Documents new screens, marks roadmap item complete, adds changelog entry, bumps backend version.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Ticker
  participant exportScheduler
  participant ScheduleStore
  participant Server
  participant Runtime

  Ticker->>exportScheduler: tick
  exportScheduler->>ScheduleStore: ListAll()
  ScheduleStore-->>exportScheduler: schedules
  exportScheduler->>exportScheduler: filter due schedules
  exportScheduler->>Server: executeVolumeExport(request)
  Server->>Runtime: ExportVolume(ctx, opts)
  Runtime-->>Server: result / error
  Server-->>exportScheduler: export status
  exportScheduler->>ScheduleStore: Save(updated schedule)
Loading
sequenceDiagram
  participant User
  participant VolumeDetailView
  participant ApiClient
  participant FileSystem

  User->>VolumeDetailView: tap download
  VolumeDetailView->>ApiClient: downloadVolumeExport(volumeName, exportId)
  ApiClient-->>VolumeDetailView: bytes
  VolumeDetailView->>User: getSaveLocation prompt
  User-->>VolumeDetailView: chosen path
  VolumeDetailView->>FileSystem: write bytes
Loading

Possibly related PRs

  • enegalan/calf#19: Extends the same handleVolumeAction routing in backend/internal/api/volumes.go that this PR builds on to add exports/export-schedules subresources.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding volume export functionality with UI integration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 24-feat-volumes-export

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.

Resolve conflicts keeping volume export scheduler alongside build history
sync and build detail API methods.

Co-authored-by: Cursor <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (10)
ui/lib/api/client.dart (1)

641-653: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Catch only parse failures when formatting nextRunAt.

DateTime.parse should only fall back for invalid date strings; catching every exception can hide unrelated defects in this display helper. Based on learnings, “Do not write generic catch-alls; handle each specific error case individually so logs, UI, and API responses clearly identify what failed and why.”

Proposed fix
-    } catch (_) {
+    } on FormatException {
       return nextRunAt;
     }
🤖 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 `@ui/lib/api/client.dart` around lines 641 - 653, The `nextRunAt` display
helper in `client.dart` is using a blanket catch that can hide non-parse bugs.
Update the formatting logic around `DateTime.parse` to handle only parsing
failures explicitly, and let unexpected exceptions surface instead of falling
back silently. Keep the existing `nextRunAt` formatting path intact for valid
dates, and only return the raw `nextRunAt` value when the parse itself fails.

Source: Learnings

ui/lib/screens/volume_quick_export_screen.dart (1)

55-79: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Generic catch-alls surface raw error strings to the UI.

_loadImages, _browseFolder, and _save all fall back to a bare catch (error) that displays error.toString() directly to the user. This repeats across the file and doesn't distinguish network failures, validation errors, or backend error codes, making it hard for users/logs to pinpoint the actual cause.

Based on learnings, "Do not write generic catch-alls; handle each specific error case individually so logs, UI, and API responses clearly identify what failed and why."

Also applies to: 97-122, 124-160

🤖 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 `@ui/lib/screens/volume_quick_export_screen.dart` around lines 55 - 79, The
error handling in _loadImages, _browseFolder, and _save is using a generic catch
and surfacing error.toString() directly to the UI. Update the exception handling
in VolumeQuickExportScreen to catch and map specific failure cases separately
(for example network, validation, and backend/API errors), and set a user-facing
message that reflects the actual failure type instead of the raw exception
string. Keep the mounted checks and apply the same pattern consistently across
the relevant methods so the UI and logs clearly identify what failed and why.
ui/lib/screens/volume_schedule_export_screen.dart (1)

103-127: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Same generic catch-all pattern repeated here.

Same issue flagged in volume_quick_export_screen.dart: every async operation (_loadImages, _browseFolder, _applyChanges, _confirmDelete) falls back to a bare catch (error) and shows error.toString() to the user, rather than handling specific failure modes.

Based on learnings, "Do not write generic catch-alls; handle each specific error case individually so logs, UI, and API responses clearly identify what failed and why."

Also applies to: 258-283, 341-395, 397-445

🤖 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 `@ui/lib/screens/volume_schedule_export_screen.dart` around lines 103 - 127,
The async flow in _loadImages still uses a generic catch-all and surfaces
error.toString() directly, which should be replaced with handling for the
specific failure modes used elsewhere in VolumeScheduleExportScreen. Update
_loadImages (and the related _browseFolder, _applyChanges, and _confirmDelete
paths mentioned in the comment) to catch and branch on concrete exception types,
then set clearer UI error states and logging for each case instead of a single
broad catch (error).
backend/internal/runtime/volume_export.go (2)

71-79: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cleanup defer reuses the parent, possibly-canceled ctx.

If ctx is canceled/times out mid-export, the nerdctl rm -f cleanup in the deferred func will also fail immediately (same canceled context), leaking the staging container. Using a short-lived detached context for teardown would make cleanup reliable regardless of the caller's cancellation state.

♻️ Suggested approach
 	defer func() {
-		_, _ = run(ctx, "nerdctl", "rm", "-f", containerName)
+		cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+		defer cancel()
+		_, _ = run(cleanupCtx, "nerdctl", "rm", "-f", containerName)
 	}()
🤖 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 `@backend/internal/runtime/volume_export.go` around lines 71 - 79, The deferred
cleanup in volume export reuses the same possibly-canceled context, so the
`nerdctl rm -f` teardown can fail after caller timeout/cancellation. Update the
`defer` inside `volumeExport` to use a fresh short-lived background context for
cleanup instead of the parent `ctx`, and keep the existing `run`/`containerName`
flow unchanged so the staging container is always removed reliably.

42-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated "alpine:3.20" literal.

The base image tag is hard-coded twice. Extracting it to a package-level constant avoids the two copies drifting apart on future upgrades.

♻️ Suggested fix
+const exportBaseImage = "alpine:3.20"
+
 func exportVolumeArchive(ctx context.Context, run commandRunner, volumeName, archivePath string) error {
 	...
-		"alpine:3.20",
+		exportBaseImage,

Also applies to: 72-72

🤖 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 `@backend/internal/runtime/volume_export.go` at line 42, The alpine base image
tag is duplicated in the runtime volume export setup, so extract the hard-coded
"alpine:3.20" into a single package-level constant and reuse it in the relevant
places. Update the values referenced from the volume export code path (including
the image list and any other use in volume_export.go) to point to that constant
so future version bumps stay in sync.
backend/internal/runtime/mock.go (1)

205-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuses ContainerErr for a volume-export failure.

ExportVolume returns m.ContainerErr for a generic failure trigger, but that field is otherwise scoped to container operations elsewhere in Mock. Reusing it here couples unrelated failure domains and prevents a test from simulating a container error and an export error independently, obscuring what a given test failure is actually exercising.

♻️ Suggested fix: add a dedicated error field
 type Mock struct {
 	...
 	ContainerErr   error
 	ImageErr       error
+	ExportErr      error
 	...
 }

 func (m *Mock) ExportVolume(_ context.Context, opts VolumeExportOptions) (string, error) {
 	m.mu.Lock()
 	defer m.mu.Unlock()

-	if m.ContainerErr != nil {
-		return "", m.ContainerErr
+	if m.ExportErr != nil {
+		return "", m.ExportErr
 	}

Based on learnings, "Do not use generic catch-alls; handle each specific error case individually so logs, UI, and API responses identify exactly what failed and why."

🤖 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 `@backend/internal/runtime/mock.go` around lines 205 - 229, ExportVolume
currently reuses Mock.ContainerErr as a generic failure trigger, which couples
volume export failures to container errors. Add a dedicated error field on Mock
for export-volume failures and have Mock.ExportVolume check that field instead
of ContainerErr, so container and export tests can fail independently. Update
the ExportVolume method and any related mock setup helpers to use the new field
while keeping the existing validation branches for opts.Type, opts.Folder, and
opts.ImageRef unchanged.

Source: Learnings

backend/internal/api/volumes.go (1)

78-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant/unreachable OPTIONS checks.

handleVolumeAction already returns 204 for any OPTIONS request at function entry (pre-existing check, not shown in this diff range). The four new if r.Method == http.MethodOptions {...} blocks added here (for exports, export-schedules, the export-schedules/{id} branch, and the exports/{id}/download branch) can never execute, since any OPTIONS request already returned earlier. This is dead code contributing to the flagged complexity.

♻️ Proposed cleanup
 		case "exports":
-			if r.Method == http.MethodOptions {
-				w.WriteHeader(http.StatusNoContent)
-				return
-			}
-
 			s.handleVolumeExports(w, r, name)
 			return
 		case "export-schedules":
-			if r.Method == http.MethodOptions {
-				w.WriteHeader(http.StatusNoContent)
-				return
-			}
-
 			s.handleVolumeExportSchedules(w, r, name)
 			return
 		}
 	}

 	if len(parts) == 3 && parts[1] == "export-schedules" {
-		if r.Method == http.MethodOptions {
-			w.WriteHeader(http.StatusNoContent)
-			return
-		}
-
 		s.handleVolumeExportScheduleItem(w, r, name, parts[2])
 		return
 	}

 	if len(parts) == 4 && parts[1] == "exports" && parts[3] == "download" {
-		if r.Method == http.MethodOptions {
-			w.WriteHeader(http.StatusNoContent)
-			return
-		}
-
 		s.handleVolumeExportDownload(w, r, name, parts[2])
 		return
 	}
🤖 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 `@backend/internal/api/volumes.go` around lines 78 - 114, The new OPTIONS
handling inside handleVolumeAction is unreachable because the method already
returns 204 for OPTIONS at the top of the function. Remove the redundant
http.MethodOptions checks from the exports, export-schedules,
export-schedules/{id}, and exports/{id}/download branches so the routing logic
stays simple and avoids dead code.
backend/internal/volumeexport/name_pattern_test.go (1)

1-61: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for image-ref expansion / uppercase volume names.

All tests exercise the file-name expansion path only. Adding a test for ExpandExportImageRefPattern/DefaultImageRefPattern with a mixed-case volume name would have caught the lowercase-ref bug flagged in name_pattern.go.

🤖 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 `@backend/internal/volumeexport/name_pattern_test.go` around lines 1 - 61, The
current tests in TestExpandExportNamePattern and TestResolveScheduledExportNames
only cover file-name expansion, so add coverage for image-ref generation as
well. Introduce a test that exercises ExpandExportImageRefPattern with
DefaultImageRefPattern using a mixed-case volume name and assert the result is
normalized/lowercased correctly. Use the existing volumeexport helpers and
Schedule-related paths so the new test would catch the lowercase-ref bug in
name_pattern.go.
backend/internal/api/volume_export_runner.go (1)

19-91: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No explicit timeout for API-triggered exports.

executeVolumeExport is called from handleVolumeExportCreate with the bare r.Context() (no additional deadline), while the scheduler path wraps the same call with a 30-minute context.WithTimeout. If the runtime export hangs (e.g. a stuck docker/nerdctl command) and the client keeps the connection open, the request goroutine blocks indefinitely with no bound. For consistency with the scheduler path, consider applying a similar explicit timeout in the HTTP handler as well.

As per coding guidelines, "backend/**/*.go: In Go backend code, thread a context.Context with an explicit timeout or cancellation through every handler-to-runtime path".

🤖 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 `@backend/internal/api/volume_export_runner.go` around lines 19 - 91,
`executeVolumeExport` is invoked from `handleVolumeExportCreate` without a
bounded deadline, so API-triggered exports can block indefinitely if the runtime
hangs. Update the HTTP handler path to create a timeout context before calling
`executeVolumeExport`, matching the scheduler flow that already uses
`context.WithTimeout`, and ensure the timeout is propagated through
`Server.executeVolumeExport` into `runtime.ExportVolume`.

Source: Coding guidelines

backend/internal/api/server.go (1)

40-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Constructor starts an unstoppable-by-default background goroutine with hardcoded storage paths.

New() unconditionally spins up exportScheduler (which immediately ticks and reads/writes under ~/.config/calf/... via volumeexport.NewStore/NewScheduleStore, both of which resolve os.UserHomeDir() with no override hook). Every caller of New() — including tests — now performs real filesystem I/O in the user's home directory unless it later calls Shutdown(). See the backend/test/api/api_test.go review for the concrete downstream impact (goroutine leak + real fs writes in newTestServer).

🤖 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 `@backend/internal/api/server.go` around lines 40 - 52, New() is eagerly
starting exportScheduler, which creates an always-on background goroutine and
immediately touches hardcoded home-directory storage through
volumeexport/NewStore and NewScheduleStore. Make Server construction side-effect
free by deferring scheduler creation/startup out of New() (or gating it behind
an explicit opt-in/init method), and use the existing Server fields like
cfg/runtime to add a test-friendly way to override or disable the storage path.
Ensure callers such as newTestServer can construct Server without triggering
filesystem I/O or needing Shutdown() just to avoid leaks.
🤖 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 @.cursor/rules/calf.mdc:
- Around line 125-127: Update the project tree branch markers in the rules file
so the sibling entries under the volume screens are rendered correctly. In the
tree snippet containing volume_detail_screen.dart,
volume_quick_export_screen.dart, and volume_schedule_export_screen.dart, change
the first two child markers to use the non-final branch form and keep only the
last entry as the final branch. This is just a formatting fix in the tree
listing, so adjust the markers around those three filenames accordingly.

In `@backend/internal/api/volume_export_scheduler.go`:
- Around line 39-44: `Stop()` currently waits for `runSchedule()` to finish but
the schedule context is only based on `context.Background()` with a fixed
timeout, so shutdown cannot cancel an in-flight export. Update `exportScheduler`
so `Stop()` cancels the scheduler context immediately (for example by
introducing a cancelable context tied to `s.stop` or a `context.WithCancel`
field), and have `runSchedule` use that context instead of an isolated
background timeout. Make sure the goroutine in `run()`/`runSchedule()` observes
cancellation and exits promptly so `<-s.done` returns during `Server.Shutdown`
rather than blocking until the export completes or times out.

In `@backend/internal/api/volume_export_schedules.go`:
- Around line 42-56: The HTTP handlers are returning raw internal error strings
directly via writeError, which can leak filesystem/path details to clients.
Update handleVolumeExportSchedulesList and the other affected handlers in
backend/internal/api to stop passing err.Error() straight to the response;
instead map failures through writeRuntimeError (or writeJSON where appropriate)
with the correct HTTP status code. Use the existing handler/store functions like
volumeScheduleStore and store.List as the places where errors should be
converted before writing the response.

In `@backend/internal/runtime/volume_export.go`:
- Around line 52-72: The export container name in exportVolumeToImage is not
unique because it is built from filepath.Base(archivePath), which is always the
same archive filename and can collide across concurrent or repeated exports.
Update the containerName generation in exportVolumeToImage to derive from a
unique part of the export path, such as the parent directory or another
export-specific identifier, so each nerdctl run uses a distinct name. Make the
same change in the other export path handling referenced by the related block so
local_image, new_image, and registry exports all avoid name collisions.

In `@backend/internal/volumeexport/name_pattern.go`:
- Around line 82-85: The image-ref path built from sanitizeVolumeToken is still
preserving uppercase characters, which can produce invalid Docker/nerdctl
repository names. Update the TypeNewImage and TypeRegistry flow in
name_pattern.go to apply strings.ToLower only when constructing image refs,
while leaving the file export path unchanged. Keep sanitizeVolumeToken focused
on trimming and replacing path separators, and use the lowercased token only in
the image-related naming logic.

In `@backend/internal/volumeexport/schedule.go`:
- Around line 95-130: ListAll in ScheduleStore is silently skipping unreadable
or malformed schedule files, which can hide whole volumes from
exportScheduler.tick(). Update ScheduleStore.ListAll so it does not discard
os.ReadFile/json.Unmarshal failures without notice; either return/report
skipped-file errors or surface them to the caller. If ScheduleStore stays
logger-free, adjust the caller in exportScheduler/volume_export_scheduler.go to
provide logging or handle a returned skipped-file count so operators can see
which files were ignored.

In `@backend/internal/volumeexport/store.go`:
- Around line 56-86: The Store.List method is swallowing readMeta failures and
silently skipping corrupted export entries. Update the List flow to surface or
at least log readMeta errors when iterating entries, using the existing
Store/readMeta path so operators can see which export metadata failed. Keep
valid exports included, but avoid a bare continue on error; instead add context
with volumeName and entry.Name() and return or report the failure in a visible
way.
- Around line 155-171: Reject path-traversal segments before building volume
export paths: the current sanitization in Store.volumeDir, Store.exportDir, and
sanitizeName only replaces a few characters, so a raw ".." segment from
backend/internal/api/volumes.go can still escape s.root via filepath.Join.
Harden the API boundary in the volume handlers that forward r.URL.Path into
name/exportID, or update sanitizeName to explicitly map "." and ".." to a safe
fallback like "unknown" before volumeDir/exportDir joins.

In `@CLAUDE.md`:
- Around line 123-125: The project tree branch markers under the volume screens
are incorrect because volume_detail_screen.dart is no longer the only child.
Update the entries in the tree so the first two children use the proper
branching symbol with volume_detail_screen.dart and
volume_quick_export_screen.dart, leaving the last child as the terminating
branch, and keep the existing labels aligned with those symbols.

In `@ui/lib/api/client.dart`:
- Around line 1069-1082: The create export schedule request in client.dart is
ignoring the public daysOfWeek and times parameters when dayTimes is empty, so
the payload can be missing timing data. Update the volume export schedule method
to serialize either _scheduleTimingBody(dayTimes) or, when dayTimes is not
provided, the daysOfWeek/times fields directly in the same request body. Keep
the fix inside the schedule creation method that builds the jsonEncode payload
so callers using those arguments are correctly represented.

In `@ui/lib/screens/volume_detail_screen.dart`:
- Around line 291-302: Prompt for the save location before starting the export
download in the volume detail flow, since the current VolumeDetailScreen logic
downloads first and only then calls getSaveLocation. Update the download/export
handling around _downloadingExportId so it resolves the suggested file name,
asks for the destination, and returns early if the user cancels before calling
widget.apiClient.downloadVolumeExport or File.writeAsBytes. Keep the existing
mounted/setState cleanup behavior in the cancel path.

---

Nitpick comments:
In `@backend/internal/api/server.go`:
- Around line 40-52: New() is eagerly starting exportScheduler, which creates an
always-on background goroutine and immediately touches hardcoded home-directory
storage through volumeexport/NewStore and NewScheduleStore. Make Server
construction side-effect free by deferring scheduler creation/startup out of
New() (or gating it behind an explicit opt-in/init method), and use the existing
Server fields like cfg/runtime to add a test-friendly way to override or disable
the storage path. Ensure callers such as newTestServer can construct Server
without triggering filesystem I/O or needing Shutdown() just to avoid leaks.

In `@backend/internal/api/volume_export_runner.go`:
- Around line 19-91: `executeVolumeExport` is invoked from
`handleVolumeExportCreate` without a bounded deadline, so API-triggered exports
can block indefinitely if the runtime hangs. Update the HTTP handler path to
create a timeout context before calling `executeVolumeExport`, matching the
scheduler flow that already uses `context.WithTimeout`, and ensure the timeout
is propagated through `Server.executeVolumeExport` into `runtime.ExportVolume`.

In `@backend/internal/api/volumes.go`:
- Around line 78-114: The new OPTIONS handling inside handleVolumeAction is
unreachable because the method already returns 204 for OPTIONS at the top of the
function. Remove the redundant http.MethodOptions checks from the exports,
export-schedules, export-schedules/{id}, and exports/{id}/download branches so
the routing logic stays simple and avoids dead code.

In `@backend/internal/runtime/mock.go`:
- Around line 205-229: ExportVolume currently reuses Mock.ContainerErr as a
generic failure trigger, which couples volume export failures to container
errors. Add a dedicated error field on Mock for export-volume failures and have
Mock.ExportVolume check that field instead of ContainerErr, so container and
export tests can fail independently. Update the ExportVolume method and any
related mock setup helpers to use the new field while keeping the existing
validation branches for opts.Type, opts.Folder, and opts.ImageRef unchanged.

In `@backend/internal/runtime/volume_export.go`:
- Around line 71-79: The deferred cleanup in volume export reuses the same
possibly-canceled context, so the `nerdctl rm -f` teardown can fail after caller
timeout/cancellation. Update the `defer` inside `volumeExport` to use a fresh
short-lived background context for cleanup instead of the parent `ctx`, and keep
the existing `run`/`containerName` flow unchanged so the staging container is
always removed reliably.
- Line 42: The alpine base image tag is duplicated in the runtime volume export
setup, so extract the hard-coded "alpine:3.20" into a single package-level
constant and reuse it in the relevant places. Update the values referenced from
the volume export code path (including the image list and any other use in
volume_export.go) to point to that constant so future version bumps stay in
sync.

In `@backend/internal/volumeexport/name_pattern_test.go`:
- Around line 1-61: The current tests in TestExpandExportNamePattern and
TestResolveScheduledExportNames only cover file-name expansion, so add coverage
for image-ref generation as well. Introduce a test that exercises
ExpandExportImageRefPattern with DefaultImageRefPattern using a mixed-case
volume name and assert the result is normalized/lowercased correctly. Use the
existing volumeexport helpers and Schedule-related paths so the new test would
catch the lowercase-ref bug in name_pattern.go.

In `@ui/lib/api/client.dart`:
- Around line 641-653: The `nextRunAt` display helper in `client.dart` is using
a blanket catch that can hide non-parse bugs. Update the formatting logic around
`DateTime.parse` to handle only parsing failures explicitly, and let unexpected
exceptions surface instead of falling back silently. Keep the existing
`nextRunAt` formatting path intact for valid dates, and only return the raw
`nextRunAt` value when the parse itself fails.

In `@ui/lib/screens/volume_quick_export_screen.dart`:
- Around line 55-79: The error handling in _loadImages, _browseFolder, and _save
is using a generic catch and surfacing error.toString() directly to the UI.
Update the exception handling in VolumeQuickExportScreen to catch and map
specific failure cases separately (for example network, validation, and
backend/API errors), and set a user-facing message that reflects the actual
failure type instead of the raw exception string. Keep the mounted checks and
apply the same pattern consistently across the relevant methods so the UI and
logs clearly identify what failed and why.

In `@ui/lib/screens/volume_schedule_export_screen.dart`:
- Around line 103-127: The async flow in _loadImages still uses a generic
catch-all and surfaces error.toString() directly, which should be replaced with
handling for the specific failure modes used elsewhere in
VolumeScheduleExportScreen. Update _loadImages (and the related _browseFolder,
_applyChanges, and _confirmDelete paths mentioned in the comment) to catch and
branch on concrete exception types, then set clearer UI error states and logging
for each case instead of a single broad catch (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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bc2356a-5cb0-4939-9fd8-96026a8c1f5b

📥 Commits

Reviewing files that changed from the base of the PR and between e3935bd and 53add30.

⛔ Files ignored due to path filters (1)
  • ui/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • .cursor/rules/calf.mdc
  • CHANGELOG.md
  • CLAUDE.md
  • ROADMAP.md
  • backend/internal/api/server.go
  • backend/internal/api/volume_export_runner.go
  • backend/internal/api/volume_export_scheduler.go
  • backend/internal/api/volume_export_schedules.go
  • backend/internal/api/volume_exports.go
  • backend/internal/api/volumes.go
  • backend/internal/runtime/lima.go
  • backend/internal/runtime/mock.go
  • backend/internal/runtime/native.go
  • backend/internal/runtime/runtime.go
  • backend/internal/runtime/volume_export.go
  • backend/internal/volumeexport/name_pattern.go
  • backend/internal/volumeexport/name_pattern_test.go
  • backend/internal/volumeexport/schedule.go
  • backend/internal/volumeexport/schedule_timing.go
  • backend/internal/volumeexport/schedule_timing_test.go
  • backend/internal/volumeexport/store.go
  • backend/test/api/api_test.go
  • backend/version/version.go
  • ui/lib/api/client.dart
  • ui/lib/export_name_pattern.dart
  • ui/lib/screens/volume_detail_screen.dart
  • ui/lib/screens/volume_quick_export_screen.dart
  • ui/lib/screens/volume_schedule_export_screen.dart
  • ui/linux/flutter/generated_plugin_registrant.cc
  • ui/linux/flutter/generated_plugins.cmake
  • ui/macos/Flutter/GeneratedPluginRegistrant.swift
  • ui/macos/Runner/DebugProfile.entitlements
  • ui/macos/Runner/Release.entitlements
  • ui/pubspec.yaml
  • ui/test/widget_test.dart
  • ui/windows/flutter/generated_plugin_registrant.cc
  • ui/windows/flutter/generated_plugins.cmake

Comment thread .cursor/rules/calf.mdc Outdated
Comment thread backend/internal/api/volume_export_scheduler.go
Comment thread backend/internal/api/volume_export_schedules.go
Comment thread backend/internal/runtime/volume_export.go Outdated
Comment on lines +82 to +85
func sanitizeVolumeToken(value string) string {
replacer := strings.NewReplacer("/", "_", "\\", "_")
return replacer.Replace(strings.TrimSpace(value))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and nearby code
git ls-files backend/internal/volumeexport && echo '---'
ast-grep outline backend/internal/volumeexport/name_pattern.go --view expanded || true
echo '--- FILE ---'
cat -n backend/internal/volumeexport/name_pattern.go | sed -n '1,220p'

echo '--- SEARCH FOR IMAGE REF SANITIZATION ---'
rg -n "sanitizeVolumeToken|ExpandExportImageRefPattern|DefaultImageRefPattern|forFileExport|ToLower|lowercase|image ref|repository" backend/internal/volumeexport backend/internal -g '!**/*_test.go'

echo '--- DOCKER IMAGE NAME RULES IN REPO ---'
rg -n "lowercase|repository name|image name|Docker" . -g '!**/node_modules/**' -g '!**/vendor/**' | sed -n '1,120p'

Repository: enegalan/calf

Length of output: 22002


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- SCHEDULE / VALIDATION ---'
cat -n backend/internal/volumeexport/schedule.go | sed -n '1,240p'
echo '--- API USAGE ---'
cat -n backend/internal/api/volume_export_schedules.go | sed -n '180,280p'
echo '--- RUNTIME IMAGE REF HANDLING ---'
cat -n backend/internal/runtime/volume_export.go | sed -n '1,220p'
echo '--- IMAGE REPOSITORY HELPERS ---'
rg -n "imageRepositoryName|reference|repo" backend/internal/runtime -g '!**/*_test.go'
if rg -n "func imageRepositoryName|func .*Reference|func .*Repository" backend/internal/runtime -g '!**/*_test.go'; then
  :
fi

Repository: enegalan/calf

Length of output: 19064


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- NAME PATTERN TESTS ---'
cat -n backend/internal/volumeexport/name_pattern_test.go | sed -n '1,260p'

echo '--- SEARCH FOR UPPERCASE / SANITIZE TESTS ---'
rg -n "MyVolume|uppercase|sanitizeVolumeToken|DefaultImageRefPattern|ExpandExportImageRefPattern|TypeNewImage|TypeRegistry" backend -g '*_test.go'

echo '--- SEARCH FOR SCHEDULE VALIDATION ---'
rg -n "ValidateScheduleInput|NormalizeSchedule|sanitizeName|volume name|image reference" backend/internal/volumeexport backend/internal/api -g '!**/*_test.go'

Repository: enegalan/calf

Length of output: 2253


🌐 Web query:

Docker image reference repository names lowercase uppercase invalid repository name must be lowercase documentation

💡 Result:

In Docker, repository names must be lowercase [1][2]. If you attempt to use uppercase letters in a repository name, you will receive an error such as: "invalid reference format: repository name must be lowercase" [3][4][5]. This requirement is enforced by the Docker engine and container registries to maintain consistency and compatibility across different platforms [2]. While tags (the identifiers following the colon, e.g.,:latest) are permitted to contain both uppercase and lowercase letters [1][6][2], the repository component itself—which includes the namespace and image name—must strictly adhere to lowercase alphanumeric characters [1][7][2]. Key points regarding naming conventions: - Repository names: Must be lowercase [1][2]. They may contain digits and separators (periods, one or two underscores, or one or more dashes) but cannot start or end with a separator [1][8][2]. - Tags: Unlike repository names, tags are case-sensitive and allow uppercase letters [1][6][2]. - Enforcement: This restriction is validated at the client level (e.g., when running docker build or docker tag) and is often enforced by registry servers as well [9][5][2]. If you encounter this error, you must rename your image to use only lowercase letters to proceed [2].

Citations:


Lowercase the volume token for image refs
sanitizeVolumeToken only strips path separators, so TypeNewImage/TypeRegistry can still build refs like MyVolume-backup:.... Docker/nerdctl require the repository name to be lowercase, so uppercase volume names will make export fail. Keep file exports unchanged and apply strings.ToLower only on the image-ref path.

🤖 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 `@backend/internal/volumeexport/name_pattern.go` around lines 82 - 85, The
image-ref path built from sanitizeVolumeToken is still preserving uppercase
characters, which can produce invalid Docker/nerdctl repository names. Update
the TypeNewImage and TypeRegistry flow in name_pattern.go to apply
strings.ToLower only when constructing image refs, while leaving the file export
path unchanged. Keep sanitizeVolumeToken focused on trimming and replacing path
separators, and use the lowercased token only in the image-related naming logic.

Comment thread backend/internal/volumeexport/store.go
Comment thread backend/internal/volumeexport/store.go
Comment thread CLAUDE.md Outdated
Comment thread ui/lib/api/client.dart Outdated
Comment thread ui/lib/screens/volume_detail_screen.dart
…e export

- Enhanced error handling in volume export API methods to provide clearer feedback on failures.
- Introduced context management for volume export scheduling and execution, ensuring better control over timeouts.
- Updated volume export naming patterns to ensure consistent formatting and sanitization.
- Refactored UI components to streamline volume export interactions and improve user experience.
- Added tests for new error handling and naming pattern functionalities.
@enegalan enegalan merged commit 020cfb0 into main Jul 5, 2026
3 checks passed
@enegalan enegalan deleted the 24-feat-volumes-export branch July 5, 2026 18:36
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.

feat: Volumes export

1 participant