fix(soundboard): upload cap counts staged clips once and per batch
The guard double-counted staged uploads and read a stale count for every file in a batch, so a 60-file drop bypassed the cap while a nearly-full pack refused early. Partition the batch with a running count. Fixes #31 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -73,6 +73,32 @@ const formatClipSeconds = (seconds: number): string => {
|
|||||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Gitea #31] Pure running-count cap check for `handleFiles`: given how many
|
||||||
|
* clips already exist (staged uploads included) before this batch starts,
|
||||||
|
* decide which of the batch's files fit under `max`. Kept pure/exported so the
|
||||||
|
* "running count, not a stale double-counted closure value" logic can be unit
|
||||||
|
* tested without a DOM/MatrixClient.
|
||||||
|
*/
|
||||||
|
export function acceptClips<T>(
|
||||||
|
currentCount: number,
|
||||||
|
files: readonly T[],
|
||||||
|
max: number,
|
||||||
|
): { accepted: T[]; rejected: T[] } {
|
||||||
|
const accepted: T[] = [];
|
||||||
|
const rejected: T[] = [];
|
||||||
|
let count = currentCount;
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (count >= max) {
|
||||||
|
rejected.push(file);
|
||||||
|
} else {
|
||||||
|
accepted.push(file);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { accepted, rejected };
|
||||||
|
}
|
||||||
|
|
||||||
type ClipDraft = {
|
type ClipDraft = {
|
||||||
url: string;
|
url: string;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -186,11 +212,19 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack
|
|||||||
...existing.map((c) => c.shortcode),
|
...existing.map((c) => c.shortcode),
|
||||||
...uploads.map((u) => u.shortcode),
|
...uploads.map((u) => u.shortcode),
|
||||||
]);
|
]);
|
||||||
for (let i = 0; i < files.length; i += 1) {
|
// [Gitea #31] `clipCount` already includes staged `uploads`, so don't
|
||||||
const file = files[i];
|
// add `uploads.length` again here (double-counting). And since
|
||||||
if (clipCount + uploads.length >= SOUNDBOARD_MAX_CLIPS) {
|
// `setUploads` inside the loop doesn't update this closure's
|
||||||
throw new Error(`Soundboard is full (max ${SOUNDBOARD_MAX_CLIPS} clips).`);
|
// `clipCount`, track the running total in a local variable that starts
|
||||||
}
|
// from the real current total instead of re-reading a stale value for
|
||||||
|
// every file in the batch.
|
||||||
|
const { accepted, rejected } = acceptClips(
|
||||||
|
clipCount,
|
||||||
|
Array.from(files),
|
||||||
|
SOUNDBOARD_MAX_CLIPS,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < accepted.length; i += 1) {
|
||||||
|
const file = accepted[i];
|
||||||
if (file.size > SOUNDBOARD_MAX_CLIP_BYTES) {
|
if (file.size > SOUNDBOARD_MAX_CLIP_BYTES) {
|
||||||
throw new Error(`"${file.name}" is too large (max 1 MB).`);
|
throw new Error(`"${file.name}" is too large (max 1 MB).`);
|
||||||
}
|
}
|
||||||
@@ -215,6 +249,9 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
if (rejected.length > 0) {
|
||||||
|
throw new Error(`Soundboard is full (max ${SOUNDBOARD_MAX_CLIPS} clips).`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Upload failed.');
|
setError(e instanceof Error ? e.message : 'Upload failed.');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user