11import type { SupabaseClient } from "@supabase/supabase-js" ;
22import { createClient } from "@supabase/supabase-js" ;
3- import postgres from "postgres" ;
43
54import { env } from "@/env" ;
65
76const BUCKET_NAME = "blog" ;
87
9- function getDbClient ( ) {
10- return postgres ( env . DATABASE_URL , { prepare : false } ) ;
11- }
12-
138export interface MediaItem {
149 name : string ;
1510 path : string ;
@@ -185,32 +180,64 @@ export async function uploadMediaFile(
185180 }
186181}
187182
183+ async function listAllFilesInFolder (
184+ supabase : SupabaseClient ,
185+ folderPath : string ,
186+ ) : Promise < string [ ] > {
187+ const allFiles : string [ ] = [ ] ;
188+
189+ const { data } = await supabase . storage
190+ . from ( BUCKET_NAME )
191+ . list ( folderPath , { limit : 1000 } ) ;
192+
193+ if ( ! data ) return allFiles ;
194+
195+ for ( const item of data ) {
196+ const itemPath = folderPath ? `${ folderPath } /${ item . name } ` : item . name ;
197+ const isFolder = item . id === null ;
198+
199+ if ( isFolder ) {
200+ const nestedFiles = await listAllFilesInFolder ( supabase , itemPath ) ;
201+ allFiles . push ( ...nestedFiles ) ;
202+ } else {
203+ allFiles . push ( itemPath ) ;
204+ }
205+ }
206+
207+ return allFiles ;
208+ }
209+
188210export async function deleteMediaFiles (
189211 supabase : SupabaseClient ,
190212 paths : string [ ] ,
191213) : Promise < { success : boolean ; deleted : string [ ] ; errors : string [ ] } > {
192214 const deleted : string [ ] = [ ] ;
193215 const errors : string [ ] = [ ] ;
194- const sql = getDbClient ( ) ;
195216
196217 try {
197218 for ( const path of paths ) {
198- const isFolder =
199- (
200- await sql `
201- SELECT COUNT(*) as count FROM storage.objects
202- WHERE bucket_id = ${ BUCKET_NAME }
203- AND name LIKE ${ path + "/%" }
204- `
205- ) [ 0 ] . count > 0 ;
219+ const { data : folderContents } = await supabase . storage
220+ . from ( BUCKET_NAME )
221+ . list ( path , { limit : 1 } ) ;
222+
223+ const isFolder = folderContents && folderContents . length > 0 ;
206224
207225 if ( isFolder ) {
208- await sql `
209- DELETE FROM storage.objects
210- WHERE bucket_id = ${ BUCKET_NAME }
211- AND (name = ${ path } OR name LIKE ${ path + "/%" } )
212- ` ;
213- deleted . push ( path ) ;
226+ const allFiles = await listAllFilesInFolder ( supabase , path ) ;
227+
228+ if ( allFiles . length > 0 ) {
229+ const { error } = await supabase . storage
230+ . from ( BUCKET_NAME )
231+ . remove ( allFiles ) ;
232+
233+ if ( error ) {
234+ errors . push ( `${ path } : ${ error . message } ` ) ;
235+ } else {
236+ deleted . push ( path ) ;
237+ }
238+ } else {
239+ deleted . push ( path ) ;
240+ }
214241 } else {
215242 const { data, error } = await supabase . storage
216243 . from ( BUCKET_NAME )
@@ -239,18 +266,14 @@ export async function deleteMediaFiles(
239266 deleted,
240267 errors : [ `Delete failed: ${ ( error as Error ) . message } ` ] ,
241268 } ;
242- } finally {
243- await sql . end ( ) ;
244269 }
245270}
246271
247272export async function createMediaFolder (
248- _supabase : SupabaseClient ,
273+ supabase : SupabaseClient ,
249274 folderName : string ,
250275 parentFolder : string = "" ,
251276) : Promise < { success : boolean ; path ?: string ; error ?: string } > {
252- const sql = getDbClient ( ) ;
253-
254277 const sanitizedFolderName = folderName
255278 . replace ( / [ ^ a - z A - Z 0 - 9 - _ ] / g, "-" )
256279 . toLowerCase ( ) ;
@@ -259,22 +282,27 @@ export async function createMediaFolder(
259282 ? `${ parentFolder } /${ sanitizedFolderName } `
260283 : sanitizedFolderName ;
261284
285+ const placeholderPath = `${ folderPath } /.emptyFolderPlaceholder` ;
286+
262287 try {
263- const existing = await sql `
264- SELECT id FROM storage.objects
265- WHERE bucket_id = ${ BUCKET_NAME }
266- AND name LIKE ${ folderPath + "/%" }
267- LIMIT 1
268- ` ;
269-
270- if ( existing . length > 0 ) {
288+ const { data : existing } = await supabase . storage
289+ . from ( BUCKET_NAME )
290+ . list ( folderPath , { limit : 1 } ) ;
291+
292+ if ( existing && existing . length > 0 ) {
271293 return { success : false , error : "Folder already exists" } ;
272294 }
273295
274- await sql `
275- INSERT INTO storage.objects (bucket_id, name, owner, metadata)
276- VALUES (${ BUCKET_NAME } , ${ folderPath + "/.folder" } , NULL, '{"mimetype": "application/x-directory"}')
277- ` ;
296+ const { error } = await supabase . storage
297+ . from ( BUCKET_NAME )
298+ . upload ( placeholderPath , new Uint8Array ( 0 ) , {
299+ contentType : "application/x-empty" ,
300+ upsert : false ,
301+ } ) ;
302+
303+ if ( error ) {
304+ return { success : false , error : error . message } ;
305+ }
278306
279307 return {
280308 success : true ,
@@ -285,8 +313,6 @@ export async function createMediaFolder(
285313 success : false ,
286314 error : `Failed to create folder: ${ ( error as Error ) . message } ` ,
287315 } ;
288- } finally {
289- await sql . end ( ) ;
290316 }
291317}
292318
@@ -295,24 +321,31 @@ export async function moveMediaFile(
295321 fromPath : string ,
296322 toPath : string ,
297323) : Promise < { success : boolean ; newPath ?: string ; error ?: string } > {
298- const sql = getDbClient ( ) ;
299-
300324 try {
301- const filesInFolder = await sql `
302- SELECT name FROM storage.objects
303- WHERE bucket_id = ${ BUCKET_NAME }
304- AND name LIKE ${ fromPath + "/%" }
305- ` ;
325+ const { data : folderContents } = await supabase . storage
326+ . from ( BUCKET_NAME )
327+ . list ( fromPath , { limit : 1 } ) ;
306328
307- const isFolder = filesInFolder . length > 0 ;
329+ const isFolder = folderContents && folderContents . length > 0 ;
308330
309331 if ( isFolder ) {
310- await sql `
311- UPDATE storage.objects
312- SET name = ${ toPath } || SUBSTRING(name FROM ${ fromPath . length + 1 } )
313- WHERE bucket_id = ${ BUCKET_NAME }
314- AND name LIKE ${ fromPath + "/%" }
315- ` ;
332+ const allFiles = await listAllFilesInFolder ( supabase , fromPath ) ;
333+
334+ for ( const filePath of allFiles ) {
335+ const relativePath = filePath . substring ( fromPath . length ) ;
336+ const newFilePath = toPath + relativePath ;
337+
338+ const { error } = await supabase . storage
339+ . from ( BUCKET_NAME )
340+ . move ( filePath , newFilePath ) ;
341+
342+ if ( error ) {
343+ return {
344+ success : false ,
345+ error : `Failed to move ${ filePath } : ${ error . message } ` ,
346+ } ;
347+ }
348+ }
316349
317350 return {
318351 success : true ,
@@ -337,7 +370,5 @@ export async function moveMediaFile(
337370 success : false ,
338371 error : `Move failed: ${ ( error as Error ) . message } ` ,
339372 } ;
340- } finally {
341- await sql . end ( ) ;
342373 }
343374}
0 commit comments