82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
// server/supabase/functions/_shared/storage-objects.ts
|
|
// Storage bucket port + the listing/removal routines shared by account-delete
|
|
// and audio-purge. Callers depend on StorageBucketPort, not on supabase-js, so
|
|
// the routines are unit-testable with an in-memory fake.
|
|
|
|
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
|
|
/** Storage API page and batch size (remove() accepts at most 100 paths well within limits). */
|
|
export const STORAGE_BATCH_SIZE = 100
|
|
|
|
export interface StorageEntry {
|
|
name: string
|
|
/** null for a folder placeholder, the object id for a file. */
|
|
id: string | null
|
|
}
|
|
|
|
export interface StorageBucketPort {
|
|
readonly bucket: string
|
|
list(prefix: string, offset: number, limit: number): Promise<StorageEntry[]>
|
|
remove(paths: string[]): Promise<void>
|
|
}
|
|
|
|
/** Every object path under prefix, depth-first, in name order. */
|
|
export async function listStorageFiles(storage: StorageBucketPort, prefix: string): Promise<string[]> {
|
|
const result: string[] = []
|
|
let offset = 0
|
|
|
|
while (true) {
|
|
const page = await storage.list(prefix, offset, STORAGE_BATCH_SIZE)
|
|
if (page.length === 0) break
|
|
|
|
for (const entry of page) {
|
|
const childPath = `${prefix}/${entry.name}`
|
|
if (entry.id) {
|
|
result.push(childPath)
|
|
} else {
|
|
result.push(...await listStorageFiles(storage, childPath))
|
|
}
|
|
}
|
|
|
|
if (page.length < STORAGE_BATCH_SIZE) break
|
|
offset += page.length
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/** Remove paths in API-sized batches; the first failing batch aborts the rest. */
|
|
export async function removeStorageObjects(
|
|
storage: Pick<StorageBucketPort, 'remove'>,
|
|
paths: readonly string[],
|
|
): Promise<void> {
|
|
for (let index = 0; index < paths.length; index += STORAGE_BATCH_SIZE) {
|
|
await storage.remove(paths.slice(index, index + STORAGE_BATCH_SIZE))
|
|
}
|
|
}
|
|
|
|
type StorageClient = Pick<SupabaseClient, 'storage'>
|
|
|
|
/** supabase-js adapter. Errors keep the storage_list_failed / storage_delete_failed codes. */
|
|
export function supabaseStorageBucket(client: StorageClient, bucket: string): StorageBucketPort {
|
|
return {
|
|
bucket,
|
|
async list(prefix, offset, limit) {
|
|
const { data, error } = await client.storage.from(bucket).list(prefix, {
|
|
limit,
|
|
offset,
|
|
sortBy: { column: 'name', order: 'asc' },
|
|
})
|
|
if (error) throw new Error(`storage_list_failed:${bucket}`)
|
|
return (data ?? []).map((entry) => ({
|
|
name: entry.name,
|
|
id: typeof entry.id === 'string' && entry.id.length > 0 ? entry.id : null,
|
|
}))
|
|
},
|
|
async remove(paths) {
|
|
if (paths.length === 0) return
|
|
const { error } = await client.storage.from(bucket).remove(paths)
|
|
if (error) throw new Error(`storage_delete_failed:${bucket}`)
|
|
},
|
|
}
|
|
}
|