Skip to content

Release v1.8.3: upload tokens, duplicate rejection, and an nginx auth fix - #748

Merged
ShaneIsrael merged 18 commits into
mainfrom
develop
Sep 23, 2026
Merged

ShaneIsrael merged 18 commits into
mainfrom
develop

Conversation

@ShaneIsrael

Copy link
Copy Markdown
Collaborator

Everything on develop since v1.8.2 (PRs #740–#747).

Upload tokens for scripts and other tools

Duplicate uploads

Fixes

  • Videos sidebar link gets a path of its own, so it stops redirecting away from the page it points at (fix: give Videos a path of its own so its sidebar link stops redirecting #745)
  • nginx now captures the video id for derived assets: the auth gate checks the same id it serves the file from instead of re-parsing the URI, and the error log stops filling with using uninitialized "video_id" warnings on every thumbnail request

ShaneIsrael and others added 18 commits September 22, 2026 14:37
Every upload is hashed once it lands on disk. When that hash matches a video
the library already holds, the new copy is removed and the request answers 409
with the existing video's id, title, and watch URL instead of creating a second
entry for the same content.

A row whose own file is missing from disk does not count as a duplicate: that
upload is a restore, and scan-video flips the row back to available. Nor does
the row whose path is the file just written, which is the same restore landing
exactly where the missing original used to live.

Applies to all four video upload routes — public and authenticated, single-shot
and chunked.

The upload card tracks a 'duplicate' status alongside done and error, shows it
in amber as "Already exists", and the batch summary names how many were already
in the library with links to open them. That alert stays up for ten seconds
rather than the usual three and a half, since a message with links in it needs
long enough to click; SnackbarAlert now takes autoHideDuration to allow it.
Adds a token-authenticated upload route so an external tool can push videos and
images into Fireshare without a password or a browser session, and gives every
account that may upload a place to mint, rename, regenerate and delete its own
tokens.

POST /api/upload/token takes one video or image per request, deciding which by
extension, and accepts the same title, folder, game and tag options the browser
routes take. It reuses the helpers behind /api/upload and /api/upload/image
rather than reimplementing them, so filename sanitising, the extension
allowlist, folder containment, the demo size cap, duplicate rejection and
uploader attribution are the same code paths the UI already exercises. A `game`
name is accepted alongside `game_id`, resolved against existing games only,
since a machine rarely knows a row id.

GET on the same route validates a token and reports the owner and accepted
types, so a tool can check its configuration without uploading anything.

A token carries no permissions of its own. Every request re-reads the owner's
account, so revoking upload, disabling the account, or deleting it stops the
token on the next call. Only the sha256 of the secret is stored — the raw value
exists in exactly one response, the one that mints it — and the credential is
read from a header only, never a query string, which would land in proxy access
logs and shell history. An unknown token, a revoked permission and a disabled
account are all reported identically. Repeated bad tokens from one address are
throttled, deliberately in a separate bucket from login_throttle so a tool
looping on a stale token cannot lock people out of signing in.

Tokens never expire; they end when deleted or regenerated. Deleting a user
deletes their tokens with them.

The management UI lives in Settings > Security, shown only to accounts holding
the upload permission, and reveals each new secret once with a copy button and
a ready-to-paste curl example.
Adds the two things an unattended uploader needs beyond a single POST: a way to
send a large file in pieces, and a way to find out what folders and games it may
name.

POST /api/upload/token/chunked takes one chunk per request, in any order, grouped
by a caller-chosen checkSum. Every request but the last answers 202; whichever
completes the set reassembles the file, verifies it against the declared
fileSize, and answers exactly as the single-shot route does — including the 409
when the finished video is already in the library. Both video and image uploads
can be chunked, since the finished file's extension decides which it is.

Both upload routes now funnel through _prepare_upload / _finish_upload, so they
cannot drift apart in what they accept, where they file it, or how they
attribute it. Unlike the browser-facing chunked routes, malformed chunkPart,
totalChunks or fileSize values answer 400 rather than raising through to a 500:
a token route is driven by somebody else's code, so bad input is expected.
totalChunks is capped, because every chunk is a request and a file on disk.

An abandoned chunked image upload leaves its parts under the image root rather
than the video root, so the startup sweep that removes orphaned .partNNNN files
now covers both.

GET /api/upload/token/options returns the video and image folders, the default
folder, and every game in the library. Games are listed in full rather than
through /api/games, which hides games with nothing linked to them yet: those are
exactly the ones an upload might be the first to use, and `game` name resolution
already accepts them.
Reject uploads of videos already in the library
Upload tokens for scripts and other tools
Three failures a long-running uploader hits and one thing it could not ask.

Reassembly wrote straight to the final filename, so a process killed partway
through the copy left a truncated file under a real media extension. Nothing
sweeps that -- the glob only matched .partNNNN -- while the scheduled
bulk-import picks it up within minutes and ingests it as a genuine video. It
now assembles under `{checkSum}.assembling` and renames into place once the
size check passes, which is atomic within a filesystem; the staging file sits
in the destination directory precisely so that holds.

The 202 said only "not done yet", which is indistinguishable from "I have
none of your parts". A client whose set had been swept would send its
remaining chunks, get 202 for each, run out, and hang forever with no error.
It now reports how many parts are actually held, so a caller that has sent
more than that knows to start the file over.

The startup sweep removed every part file unconditionally, which meant any
restart during an upload destroyed it. Parts survive a restart perfectly well
on their own, so the sweep is now keyed on mtime with a 24h cut -- still
collecting sets abandoned by a tool that never came back, since each part
carries the mtime of the moment it was written and an upload in progress is
nowhere near the threshold. `.assembling` files are swept on the same terms.

Adds GET /api/upload/token/exists?video_id=<hex>. Duplicate rejection only
fires once the file is on disk, so a chunked upload of something already in
the library crosses the network in full before the 409. A tool that hashes
its own file can now ask first. Matches _reject_duplicate's semantics: a row
whose file is missing from disk does not count, because that upload is a
restore.

Also corrects the docs, which told tool authors to send `folder` "with
whichever chunk you like". That is the one field where it breaks --
_prepare_upload derives the parts directory from it, so a chunk that omits it
lands in the default folder and the completing request never finds a full
set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lity

Make chunked token uploads survive a restart
Fireshare already maps folders to games: anything scanned in a folder carries
that folder's game. The mapping was readable only with a session, so a tool
holding an upload token could not see it — and a tool deciding where to *put* a
file needs exactly that mapping, read the other way round. Filing a clip in the
folder its game already owns is how it ends up tagged without the upload having
to name a game at all.

Added to /api/upload/token/options rather than given a route of its own: that
endpoint already answers "what may I choose", folder rules are the same kind of
answer, and a caller that wants them wants the folder list in the same breath.

Image folder rules are listed separately, matching how the folders themselves
are. Rules whose game has gone are left out, since there is nothing a caller
could do with one.
Tell tools which folder each game's media belongs in
The Videos page had no route of its own: it lived at "/", which is also the
landing route that forwards a visitor to whichever page sits at the top of the
sidebar. Home ships at the top of that order, so the sidebar's Videos link
pointed at "/" and was immediately bounced to /home. There was no way to reach
Videos from the sidebar at all.

Videos now lives at /videos and "/" is purely the landing redirect, so a link to
a page never passes back through the question of which page comes first.
LandingRoute loses its children along with the special case that rendered them.

The fallback landingHref returns when every page has been hidden moves from "/"
to /videos for the same reason: pointing the landing redirect at itself would
now loop.

"/" keeps forwarding with the query string attached, so existing links and
bookmarks still land somewhere sensible. The two navigate('/') calls left — the
logo and the redirect after login — mean "go to the landing page" and are
unchanged.
fix: give Videos a path of its own so its sidebar link stops redirecting
/api/upload/token/exists took only video_id. Images needed it more: a
video at least gets a 409 once its bytes are on disk, but an image is
never rejected at all — the upload is accepted and scan_image quietly
folds it into the existing row, so a client pays for the whole transfer
and is told it succeeded.

Pass exactly one of video_id or image_id; both or neither is a 400. An
image_id on an instance with images turned off is a 503. The video
response shape is unchanged.

The docs claimed images were not deduplicated. They are — scan_image has
always matched on image_id — it just never said so to the caller.
Let a tool ask about an image before uploading it, too
A token on its own does nothing: someone who wants their clips uploaded
automatically still has to go and write the thing that does the uploading, and
writing it yourself reads as the only option when nothing says otherwise.
Firesync is that thing, already built, and this is the page a person is looking
at when the question occurs to them.

The card sits between the explanation and the token list rather than below it,
so it is also there for the account that has no tokens yet — the one most likely
to be deciding how to approach this at all. It links to the releases page, since
what they want from here is the download.

Trimmed the pane's opening copy while nearby. The permission semantics it spelled
out are in docs/UploadTokens.md, which the paragraph already links to, and saying
it twice cost more attention than it earned.
feat: point people at Firesync from the Upload Tokens pane
The /_content/derived/ location ran auth_request without capturing an id
from its regex, so $video_id was unset when the auth subrequest built its
X-Fireshare-Video-Id header. Every derived asset request logged "using
uninitialized video_id" and the gate fell back to re-parsing X-Original-URI.

Capture the first path segment like the video locations already do, so the
gate checks the same id nginx serves the file from. Also initialise the
variable in the dev config's prefix location, which had the same warning.
@ShaneIsrael ShaneIsrael changed the title Upload tokens for external tools, duplicate rejection, and an nginx auth fix Release v1.8.3: upload tokens, duplicate rejection, and an nginx auth fix Sep 23, 2026
@ShaneIsrael
ShaneIsrael merged commit 6d52814 into main Sep 23, 2026
10 checks passed
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