Skip to content

Api cms delete and migrate media storage - #1909

Closed
johan-bell wants to merge 8 commits into
mainfrom
api-cms-delete-and-migrate-media-storage
Closed

Api cms delete and migrate media storage#1909
johan-bell wants to merge 8 commits into
mainfrom
api-cms-delete-and-migrate-media-storage

Conversation

@johan-bell

Copy link
Copy Markdown
Collaborator

No description provided.

ivanslabbert and others added 8 commits August 4, 2026 16:38
- Refactored MediaEditor component to remove legacy media upload functionality.
- Added useMediaEncoder composable to manage encoding sessions with Luminary Media Convert.
- Implemented health checks and session management for the encoder.
- Created utility functions for handling encoder session keys and events.
- Updated API to fetch encoder configuration for media buckets.
- Enhanced media DTO to include HLS URL and key for encoded media.
- Added tests for media encoder utilities and session management.
processMediaDto no longer processes uploads, and MediaDto has no
fileCollections or uploadData, so two validateChangeRequest tests were
asserting against a shape the DTO rejects. Replaced with the current
contract: a collection validates with an hlsUrl (plus optional hlsKey),
and a key with no URL fails on hlsUrl.

Also drop a now-unused MediaType import that failed lint.

fix(app): skip player setup when there is no <video> element

videojs() throws on an element it does not recognise, and inside the
mounted hook that surfaces as an unhandled rejection rather than
something the caller can catch. The ref is empty whenever the element
was not rendered, which is why mounting the player for content whose
video comes from parentMedia.hlsUrl failed the test run while every
assertion still passed.
Deleting a post left its whole HLS collection in the bucket permanently:
hundreds of objects, the playlists, the chunk chains, the sprites and the
sidecars. Images have always been cleaned up by deleteImage three lines away;
media was the only asset type that leaked.

Opt-in, carried on the document. A delete *is* a change request — the whole
document arrives with deleteReq set — so `media.deleteFiles` travels with the
thing it applies to and cannot be separated from it in flight. Write-only and
never persisted, the same shape as `media.hlsKey`.

The location comes from the *saved* document and the intent from the incoming
one, so an hlsUrl edited in the same breath as the delete cannot redirect the
deletion somewhere else.

Resolving the prefix is where the safety lives, and the guard falls out of the
arithmetic rather than being bolted on: the only way to get an object key from a
public URL is to strip the bucket's own public base, so a URL that does not start
with that base cannot be resolved at all. That is the "never delete a prefix we
did not create" rule, and unlike a marker object it also protects every
collection already in a bucket. On top of it: the key must end in
/master.m3u8, must not be the bucket root, must contain no traversal, and its
last folder must be a session id — because hlsUrl is an editable field, and
without that last check someone could paste a URL naming a shared folder and turn
"delete everything under it" into a tick box.

A refusal is a warning, not an error, and so is a failure part-way: the caller is
deleting a document, and refusing to do that because a bucket was unreachable
would be worse than leaving objects behind. Every key is logged before removal,
so a deletion that turns out to be wrong can be reconstructed rather than guessed
at.

Adds S3Service.listObjectsUnder: the existing listObjects returns the whole
bucket as a stream, which cannot answer "what belongs to this collection".

The resolver's tests need no database — deliberately, since the guards are the
part that must never regress and CouchDB is not always up.
A checkbox in the delete confirmation, shown only when the document actually has
a collection, with the URL underneath so it is clear what goes.

A checkbox rather than a toggle: a toggle reads as a setting applied the moment
it moves, and this is a choice being confirmed. Unticked by default, and reset
every time the dialog opens — an irreversible option that remembers a previous
"yes" is one somebody eventually triggers without meaning to. Matches the
existing "Duplicate image" checkbox in the duplicate dialog two blocks below.

The answer rides on the document as `media.deleteFiles`, which the API reads and
never stores, because a delete already sends the whole document with `deleteReq`
set. Nothing to clean up afterwards either way.
The Vite SSG work (#1686) rewrote SingleContent.vue and reworked ContentTile.vue
around it. Both conflicts were in the import blocks alone — the functional hunks
merged on their own — so main's versions stand and the videoSource changes are
re-applied on top: hasVideoSource/videoSourceFor pick the encoder's
parentMedia.hlsUrl over a typed-in `video` URL, in the tile's media icons and
progress bar and in the article page's VideoPlayer guard. Main's one-line
readingTime import is kept.
Changing `mediaBucketId` used to move nothing: `processMedia` only stored the
key, so the document ended up naming one bucket while `hlsUrl` still pointed at
another. That is worse than untidy — `resolveCollectionPrefix` derives the
storage prefix by stripping the bucket's own public base off the URL, so once the
two disagree the collection can no longer be resolved, and deleting the document
with "delete the files" ticked refuses and leaves the objects behind for good.
Images have migrated between buckets since they were added; media never did.

The order is the design: copy the whole collection, check each object's size
against the source, only then rewrite `hlsUrl`, and only then delete the
originals. A collection is not a set of independent files — a master playlist
without its segments is a broken video — so a per-file "upload then delete" like
`migrateImagesBetweenBuckets` is not enough here, where a partial result costs a
video rather than one thumbnail. Any failure leaves the source whole and
untouched and reverts `mediaBucketId`, so the document keeps pointing at files
that exist.

Copies stream. `uploadFile` takes a Buffer, which is right for a thumbnail and
wrong for a byte-range chunk chain capped at 500 MB by default, so `putStream`
was added alongside it and takes the size from the source's own `statObject`.

Refusals are inherited rather than restated: the same session-id proof that
guards deletion decides whether a collection may be moved. Changing the URL and
the bucket in one save is read as repointing the document by hand and moves
nothing, which keeps a deliberate edit from being overwritten.
@johan-bell

Copy link
Copy Markdown
Collaborator Author

Closing as already landed. Its media-migration work is on 1878-api-cms-hls-media-data-model via the squash-merge of #1896 — diffing this head against the integration branch shows no contribution left, only the affinity work this branch is missing. It also targets main, where the integration branch is the merge target while it is open.

Superseded by #1910.

@johan-bell johan-bell closed this Aug 18, 2026
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.

2 participants