Skip to content

feat(api): add gated HTTP endpoint to push files to the upload queue#216

Merged
javi11 merged 1 commit intomainfrom
claude/nostalgic-bose-521033
Apr 26, 2026
Merged

feat(api): add gated HTTP endpoint to push files to the upload queue#216
javi11 merged 1 commit intomainfrom
claude/nostalgic-bose-521033

Conversation

@javi11
Copy link
Copy Markdown
Owner

@javi11 javi11 commented Apr 25, 2026

Summary

Adds a third entry point to the upload queue: a gated HTTP endpoint so external automation (Sonarr/Radarr/scripts) can push files directly without needing a watched folder.

POST /api/v1/queue/upload
X-API-Key: <generated key>

{
  \"file\": \"/mnt/local/Media/Movie/Movie 1/movie.mkv\",
  \"relative_path\": \"/mnt/local\",
  \"delete_after_upload\": false,
  \"priority\": 0
}

relative_path is the root prefix to strip — it maps directly onto the processor's existing inputFolder, so the NZB lands at <output_dir>/Media/Movie/Movie 1/movie.nzb and the source tree is preserved.

What changed

Queue / processor

  • queue.FileJob gains optional InputFolder + DeleteOriginal *bool. New AddFileWithOptions(ctx, path, size, AddOptions{...}) threads them through goqite. Existing AddFile / AddFileWithPriority paths are untouched.
  • Processor honors job.InputFolder for relative-path output and job.DeleteOriginal as a per-job override of the global delete-original setting.
  • New QueueConfig.MinSizeToStart gates pickup behind a cumulative pending-bytes floor (0 disables). Processor calls queue.PendingTotalSize before each Receive.

Auth / API

  • New APIConfig{Enabled}. The key itself lives in a new single-row api_keys SQLite table (migration 006_add_api_keys.sql), not in config.yaml.
  • New internal/apikey package: Generate (32 bytes → base64 URL-safe), Store, EnsureKey (idempotent), Regenerate.
  • On startup, when api.enabled = true, EnsureKey runs after the queue is wired so the gated endpoint always has something to validate against.
  • Wails: App.GetAPIKey() / App.RegenerateAPIKey() for the desktop UI.
  • HTTP: middleware accepts X-API-Key (preferred) or Authorization: Bearer <key>, compares with subtle.ConstantTimeCompare. Returns 403 when the API is disabled and 401 on missing/invalid keys. Only /api/v1/queue/upload is gated; existing /api/* UI routes remain open. Two open helper routes (/api/api-key, /api/api-key/regenerate) back the settings page in web mode.
  • Validation: rejects directories, missing files, non-absolute paths, and relative_path values that aren't a parent prefix of file — so we never emit an NZB at the output root by accident.

Frontend

  • New ApiSection.svelte (Automation tab): enable toggle, key display with reveal/copy/regenerate, and a copy-pasteable curl example.
  • QueueSection.svelte extended with min_size_to_start using the existing ByteSizeInput (presets: disabled, 50/100/200/500 GB).
  • Wails bindings (App.js, App.d.ts, models.ts) regenerated to expose the new methods + config fields.
  • API client glue in client.ts / web-client.ts for getApiKey() / regenerateApiKey().
  • i18n keys for settings.api.* and settings.queue.min_size_to_start* added across en/es/fr/tr.

Notes for reviewers

  • Key persistence intentionally uses SQLite, not config.yaml, so rotation doesn't churn the user's config file and the key isn't accidentally committed via configuration backups.
  • Folder uploads via the API are out of scope for this PR (handler rejects directories) — the existing FOLDER: prefix is still available to internal call sites.
  • Backward compatibility: existing FileJob JSON in goqite deserializes cleanly because the new fields are pointer/zero-value optional.

Test plan

  • go test -race ./internal/queue/... ./internal/apikey/... ./internal/processor/... ./internal/watcher/... ./pkg/... passes
  • go vet ./internal/... ./pkg/... ./cmd/postie/... ./cmd/web/... clean
  • bun run check (svelte-check): 0 errors, 0 warnings
  • Manual smoke (web mode): enable the API in Settings → Automation, copy the key, then curl -X POST -H \"X-API-Key: <key>\" -d '{\"file\":\"...\",\"relative_path\":\"...\"}' http://localhost:8080/api/v1/queue/upload and confirm:
    • The queue picks up the file and produces an NZB at the expected path
    • With delete_after_upload: true, the source file is removed after success
    • With queue.min_size_to_start set, processing waits until the threshold is met
    • 401 on missing/wrong key, 403 when api.enabled = false, 400 on mismatched relative_path
  • Manual smoke (Wails desktop): regenerate the key from settings, confirm the displayed value updates and the new key authenticates

Adds POST /api/v1/queue/upload so external tools (Sonarr/Radarr/scripts)
can enqueue files with an explicit relative_path root. The processor
preserves the directory tree below that root in the resulting NZB
output, mirroring how the watcher behaves but without needing a watch
folder.

- queue.FileJob gains optional InputFolder + DeleteOriginal overrides;
  new AddFileWithOptions threads them through goqite without touching
  existing AddFile/AddFileWithPriority callers.
- processor honors job.InputFolder for relative-path output and
  job.DeleteOriginal for per-job source deletion.
- New QueueConfig.MinSizeToStart gates pickup behind a cumulative
  pending-bytes floor (0 disables); processor checks PendingTotalSize
  before each Receive.
- New APIConfig{Enabled} plus an api_keys SQLite table (single row).
  internal/apikey generates a 32-byte URL-safe key on first start,
  exposes Wails GetAPIKey/RegenerateAPIKey for the desktop UI.
- cmd/web mounts the gated endpoint behind a constant-time
  X-API-Key/Bearer middleware (403 when disabled, 401 on bad key) and
  exposes open /api/api-key + /api/api-key/regenerate routes for the
  settings page.
- Frontend: new ApiSection (toggle, key reveal/copy/regenerate, curl
  example), QueueSection extended with ByteSizeInput for
  min_size_to_start, Wails bindings + models updated, en/es/fr/tr i18n.

go test -race ./... and svelte-check both clean.
@javi11 javi11 merged commit 6b652e8 into main Apr 26, 2026
3 checks passed
@javi11 javi11 deleted the claude/nostalgic-bose-521033 branch April 26, 2026 18:56
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.

1 participant