Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ platform support that the release configuration does not provide.

## Gotchas

- `~/.rawback/{config.yml,credentials.json,upload-state.json,cameras.json}` and
`~/.rawback/logs/` are shared with the Desktop app at runtime. Changing a
- `~/.rawback/{config.yml,credentials.json,upload-state.json,upload-ledger.jsonl,cameras.json}`
and `~/.rawback/logs/` are shared with the Desktop app at runtime. Changing a
file's shape here breaks Desktop, and the contract is owned by `@rawback/sdk`,
not by this repo. `rawback logs purge` clears Desktop's log too, by design.
- `@rawback/sdk` and `@rawback/ccapi-js` are pinned to exact versions. Bumping
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,13 +473,14 @@ anything is written.

Rawback stores local state under `~/.rawback/`:

| File | Purpose |
| ------------------- | -------------------------------------------------------------- |
| `credentials.json` | Access and refresh tokens per environment, from `rawback auth` |
| `config.yml` | Environments, optional hosts, metadata workers, and SFTP |
| `upload-state.json` | Shared upload queue, history, and trusted host keys |
| `cameras.json` | Saved Canon cameras, shared with Rawback Desktop |
| `logs/` | Rolling JSON diagnostics, shared with Rawback Desktop |
| File | Purpose |
| --------------------- | -------------------------------------------------------------- |
| `credentials.json` | Access and refresh tokens per environment, from `rawback auth` |
| `config.yml` | Environments, optional hosts, metadata workers, and SFTP |
| `upload-state.json` | Shared upload queue, history, and trusted host keys |
| `upload-ledger.jsonl` | Files already in your library, shared with Rawback Desktop |
| `cameras.json` | Saved Canon cameras, shared with Rawback Desktop |
| `logs/` | Rolling JSON diagnostics, shared with Rawback Desktop |

`cameras.json` is shared with the Rawback desktop app, so both can reach the same
camera without pairing twice. It holds a camera password only when you pass
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ rawback photos upload --path <file-or-directory> [options]
| `--path <path>` | Required image/RAW file or recursively scanned directory | — |
| `--concurrency <number>` | Parallel SFTP transfers; integer from 1 through 16 | `4` |
| `--dry-run` | Report pending/skipped files, bytes, and estimated duration | `false` |
| `--recheck` | Ignore local upload history; ask the server about each file | `false` |

Upload setup and safety behavior are covered in
[Configuration and uploads](configuration.md). Unsupported files are skipped
Expand All @@ -679,6 +680,13 @@ EXIF capture time; duplicate-check failures do not block the SFTP upload.
Local EXIF workers are controlled separately by `metadata.concurrency` in the
shared configuration file.

After the transfer, the server is asked which uploaded photos it now holds; the
summary reports them as Confirmed, and any it does not have as Unconfirmed (they
upload again next time). Acknowledged files are remembered in
`~/.rawback/upload-ledger.jsonl`, shared with Rawback Desktop, so the next run
skips them before reading metadata; see
[Upload acknowledgements](configuration.md#upload-acknowledgements).

## `rawback spots`

Browses public photography Spots: places where Rawback photographers published
Expand Down
40 changes: 33 additions & 7 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ rawback photos upload --path /path/to/photos --dry-run

The dry run authenticates, validates configuration and account quota, scans the
input, and checks exact remote upload identities. It does not open an SFTP connection or
modify upload state. Its time estimate uses matching local upload
modify upload state or the upload ledger. Its time estimate uses matching local upload
history when available and otherwise uses a clearly labeled 10 Mbps fallback.

Start the upload after reviewing the totals:
Expand All @@ -370,12 +370,12 @@ rawback photos upload --path /path/to/photos --concurrency 4
```

Transfer concurrency defaults to `4` and accepts integers from `1` through `16`. All
workers share one SFTP connection.

While an upload is running in an interactive terminal, the CLI shows aggregate
bytes and files, transfer speed, ETA, and up to four active filenames. When
stdout is redirected, it emits line-oriented file status and a final summary
instead of terminal animation.
workers share one SFTP connection.

Before connecting, the CLI verifies:

Expand Down Expand Up @@ -404,14 +404,40 @@ Supported image extensions are `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.tif`,
and `.x3f`. Matching is case-insensitive. Directory scanning skips symbolic
links and unrelated file types, but fails if it finds no supported files.

## Upload acknowledgements

A successful SFTP transfer does not say whether Rawback stored the photo, found
it to be a duplicate, or failed to record it. After the transfer, the CLI asks
the server which uploaded photos it now holds (matching filename and capture
time), and the final summary reports them as **Confirmed**. A photo the server
does not have is reported as **Unconfirmed** and is uploaded again next time.

Every acknowledged file is remembered in `~/.rawback/upload-ledger.jsonl`, which
Rawback Desktop reads and writes too, for the signed-in account on the current
environment's API host. A later run — from either app — skips a remembered
file before reading its metadata or asking the server:

- A file is matched by canonical path, size, and modification time; a photo
confirmed at another path (a moved folder, a second copy) is matched by
filename and capture time.
- Photos the server confirmed, and photos without a capture time (which the
server can neither confirm nor deduplicate), are skipped outright.
- A photo whose confirmation request failed is checked with the server again
instead, without re-reading its metadata.

Pass `--recheck` to ignore the ledger and the resume state and ask the server
about every file; what it learns is still recorded.

## Resume state and interruption

Completed uploads are recorded in the portable `~/.rawback/upload-state.json`
Uploads in progress are recorded in the portable `~/.rawback/upload-state.json`
shared with Rawback Desktop. On first use, the CLI imports its legacy
`upload-progress.sqlite` records and retains that database for rollback. A later
run skips a completed file only when its canonical path, size, and modification
time still match. Incomplete files restart from the beginning because the SFTP
transport does not append partial content.
`upload-progress.sqlite` records and retains that database for rollback. A
completed file recorded there before the upload ledger existed is skipped only
when its canonical path, size, and modification time still match and it has no
capture time; other photos are checked with the server once and remembered in
the ledger from then on. Incomplete files restart from the beginning because
the SFTP transport does not append partial content.

On the first `Ctrl-C`, the CLI stops scheduling new files and lets active uploads
finish. Press `Ctrl-C` again to disconnect immediately. Re-run the same command
Expand Down
6 changes: 6 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,11 @@ export function createProgram(version: string, output = new CommandOutput()): Ar
describe: 'show upload count, size, and estimated time without uploading',
type: 'boolean',
})
.option('recheck', {
default: false,
describe: 'ask the server about every file; ignore local upload history',
type: 'boolean',
})
.check((args) => {
if (
!Number.isInteger(args.concurrency) ||
Expand All @@ -693,6 +698,7 @@ export function createProgram(version: string, output = new CommandOutput()): Ar
concurrency: args.concurrency,
dryRun: args.dryRun,
path: args.path,
recheck: args.recheck,
}),
)
},
Expand Down
10 changes: 8 additions & 2 deletions src/features/upload/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { formatBytes, formatDuration } from '../../ui/format.ts'
import { type UiDocument } from '../../ui/model.ts'

export function uploadDryRunDocument(input: {
completed: number
alreadyUploaded: number
estimatedSeconds: number
existingRemote: number
files: number
Expand All @@ -18,7 +18,7 @@ export function uploadDryRunDocument(input: {
fields: [
{ label: 'Files', value: input.files },
{ label: 'Total size', value: formatBytes(input.totalBytes) },
{ label: 'Completed', value: input.completed },
{ label: 'Already uploaded', value: input.alreadyUploaded },
{ label: 'Remote', value: input.existingRemote },
{ label: 'Estimated time', value: formatDuration(input.estimatedSeconds) },
{
Expand All @@ -34,8 +34,12 @@ export function uploadDryRunDocument(input: {

export function uploadSummaryDocument(input: {
cancelled: boolean
/** Uploaded photos the server confirmed holding. */
confirmed: number
failedFiles: number
totalBytes: number
/** Uploaded photos the server was asked about and did not have. */
unconfirmed: number
}): UiDocument {
const tone = input.cancelled || input.failedFiles > 0 ? 'warning' : 'success'
return {
Expand All @@ -54,6 +58,8 @@ export function uploadSummaryDocument(input: {
type: 'fields',
fields: [
{ label: 'Transferred', value: formatBytes(input.totalBytes) },
{ label: 'Confirmed', value: input.confirmed },
...(input.unconfirmed > 0 ? [{ label: 'Unconfirmed', value: input.unconfirmed }] : []),
{ label: 'Failed', value: input.failedFiles },
],
},
Expand Down
Loading
Loading