fix(storage): purge raw audio when its history entry or meeting is deleted

This commit is contained in:
Yun Chan 2026-09-28 02:16:22 +09:00
parent 4807a5283d
commit ed790e672b
10 changed files with 1073 additions and 39 deletions

View file

@ -0,0 +1,45 @@
// server/supabase/functions/audio-purge/policy.ts
// Pure rules for which queued storage keys the service-role worker may remove.
// storage_key is client-written (RLS only pins its first segment), so the
// worker re-checks it before acting with service-role storage access.
export const AUDIO_BUCKET = 'audio'
export const MAX_STORAGE_KEY_LENGTH = 1024
export interface PurgeQueueEntry {
id: number
userId: string
storageKey: string
leaseToken: string
}
export interface PurgePlan {
/** Entries whose object may be removed from the audio bucket. */
removable: PurgeQueueEntry[]
/** Entries dropped without touching storage (key outside the owner's prefix). */
rejected: PurgeQueueEntry[]
}
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
// deno-lint-ignore no-control-regex
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/
/** True when key is a plain object path inside the owner's own audio prefix. */
export function isPurgeableStorageKey(userId: string, key: string): boolean {
if (!UUID_PATTERN.test(userId)) return false
if (key.length === 0 || key.length > MAX_STORAGE_KEY_LENGTH) return false
if (key.includes('\\') || CONTROL_CHARACTERS.test(key)) return false
const segments = key.split('/')
if (segments.length < 2 || segments[0] !== userId) return false
return segments.every((segment) => segment.length > 0 && segment !== '.' && segment !== '..')
}
export function planPurge(entries: readonly PurgeQueueEntry[]): PurgePlan {
const removable: PurgeQueueEntry[] = []
const rejected: PurgeQueueEntry[] = []
for (const entry of entries) {
if (isPurgeableStorageKey(entry.userId, entry.storageKey)) removable.push(entry)
else rejected.push(entry)
}
return { removable, rejected }
}