fix(files): review round 1, the server — the tray's window, the GC's locks, a file's origin, the upgrade path
- The queue keeps a done job by when it became done (ready_at), not by its last write: renaming, keeping or moving an old file, or the GC's expiry warning, no longer brings it back as just finished. - GC expiry and eviction re-read each file under the asset→job locks the user routes take, and commit one file at a time: a delete, a re-download or a keep made while the GC runs is honoured, never a second release that frees a file another user still holds. - A file records the origin it was placed under (download_jobs.source_key, migration 0105): an edit whose parent expired still goes back to its folder, not to Uploads. - A folder a file is being put in is held (FOR SHARE) until it lands; a delete of it waits and takes that file home too, instead of failing on the foreign key. - The hash route reads at most two files at once (429 past that) and holds no connection meanwhile. - 0104's backfill dates old files by their request date only: no notice prod wrote names its job. - The old DOWNLOAD_ROOT still finds the files; a files root that is not a mounted volume stops the worker and is logged by the API; the volume-copy recipe looks the project-prefixed names up. - Stale comments about the /downloads mount and the Downloads page.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "./ui/Input";
|
||||
|
||||
// A name being typed in place of the name itself — a library file's in its row, a folder's in the rail
|
||||
// or the page title, a new folder's. Enter or leaving the field saves, Escape cancels: unmounting the
|
||||
// input fires no blur in React, so Escape really does cancel. The caller decides what an unchanged or
|
||||
// empty name means.
|
||||
//
|
||||
// Ended by a KEY, the field leaves with the keyboard's focus and nothing to give it to: the caller
|
||||
// puts it back on what stands there now (the row, the entry, the button that opened it) — or the
|
||||
// keyboard user is dropped on <body> and loses their place (measured in Chrome, the playlist rail's
|
||||
// rename). Ended by a blur, the user has put the focus somewhere on purpose, and it stays there.
|
||||
|
||||
export function RenameField({
|
||||
initial,
|
||||
label,
|
||||
className = "w-full",
|
||||
onDone,
|
||||
}: {
|
||||
initial: string;
|
||||
/** The field's accessible name (it has no visible label: it stands where the name stood). */
|
||||
label: string;
|
||||
className?: string;
|
||||
/** The typed name, or null when cancelled; `byKey`: Enter or Escape, so the focus is the caller's
|
||||
* to put back. */
|
||||
onDone: (name: string | null, byKey: boolean) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(initial);
|
||||
return (
|
||||
<Input
|
||||
size="sm"
|
||||
className={className}
|
||||
autoFocus
|
||||
value={draft}
|
||||
aria-label={label}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={() => onDone(draft, false)}
|
||||
onKeyDown={(e) => {
|
||||
// preventDefault: a real Enter's own activation would otherwise press what focus lands on.
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onDone(draft, true);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
// Only the edit is cancelled: a drawer (the rail on a phone) or a dialog round the field
|
||||
// must not close with it.
|
||||
e.stopPropagation();
|
||||
onDone(null, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user