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 ;
@@ -101,11 +96,6 @@ export async function uploadMediaFile(
10196 publicUrl ?: string ;
10297 error ?: string ;
10398} > {
104- const timestamp = Date . now ( ) ;
105- const sanitizedFilename = `${ timestamp } -${ filename
106- . replace ( / [ ^ a - z A - Z 0 - 9 . - ] / g, "-" )
107- . toLowerCase ( ) } `;
108-
10999 const allowedExtensions = [
110100 "jpg" ,
111101 "jpeg" ,
@@ -118,7 +108,10 @@ export async function uploadMediaFile(
118108 "webm" ,
119109 "mov" ,
120110 ] ;
121- const ext = sanitizedFilename . toLowerCase ( ) . split ( "." ) . pop ( ) ;
111+
112+ const parts = filename . split ( "." ) ;
113+ const ext = parts . pop ( ) ?. toLowerCase ( ) ;
114+ const baseName = parts . join ( "." ) . replace ( / [ ^ a - z A - Z 0 - 9 . - ] / g, "-" ) || "file" ;
122115
123116 if ( ! ext || ! allowedExtensions . includes ( ext ) ) {
124117 return {
@@ -127,7 +120,24 @@ export async function uploadMediaFile(
127120 } ;
128121 }
129122
130- const path = folder ? `${ folder } /${ sanitizedFilename } ` : sanitizedFilename ;
123+ let finalFilename = `${ baseName } .${ ext } ` ;
124+ let path = folder ? `${ folder } /${ finalFilename } ` : finalFilename ;
125+
126+ const { data : existingFiles } = await supabase . storage
127+ . from ( BUCKET_NAME )
128+ . list ( folder || undefined , { limit : 1000 } ) ;
129+
130+ if ( existingFiles ) {
131+ const existingNames = new Set ( existingFiles . map ( ( f ) => f . name ) ) ;
132+ let counter = 1 ;
133+
134+ while ( existingNames . has ( finalFilename ) ) {
135+ finalFilename = `${ baseName } -${ counter } .${ ext } ` ;
136+ counter ++ ;
137+ }
138+
139+ path = folder ? `${ folder } /${ finalFilename } ` : finalFilename ;
140+ }
131141
132142 try {
133143 const fileBuffer = Buffer . from ( content , "base64" ) ;
@@ -170,32 +180,64 @@ export async function uploadMediaFile(
170180 }
171181}
172182
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+
173210export async function deleteMediaFiles (
174211 supabase : SupabaseClient ,
175212 paths : string [ ] ,
176213) : Promise < { success : boolean ; deleted : string [ ] ; errors : string [ ] } > {
177214 const deleted : string [ ] = [ ] ;
178215 const errors : string [ ] = [ ] ;
179- const sql = getDbClient ( ) ;
180216
181217 try {
182218 for ( const path of paths ) {
183- const isFolder =
184- (
185- await sql `
186- SELECT COUNT(*) as count FROM storage.objects
187- WHERE bucket_id = ${ BUCKET_NAME }
188- AND name LIKE ${ path + "/%" }
189- `
190- ) [ 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 ;
191224
192225 if ( isFolder ) {
193- await sql `
194- DELETE FROM storage.objects
195- WHERE bucket_id = ${ BUCKET_NAME }
196- AND (name = ${ path } OR name LIKE ${ path + "/%" } )
197- ` ;
198- 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+ }
199241 } else {
200242 const { data, error } = await supabase . storage
201243 . from ( BUCKET_NAME )
@@ -224,18 +266,14 @@ export async function deleteMediaFiles(
224266 deleted,
225267 errors : [ `Delete failed: ${ ( error as Error ) . message } ` ] ,
226268 } ;
227- } finally {
228- await sql . end ( ) ;
229269 }
230270}
231271
232272export async function createMediaFolder (
233- _supabase : SupabaseClient ,
273+ supabase : SupabaseClient ,
234274 folderName : string ,
235275 parentFolder : string = "" ,
236276) : Promise < { success : boolean ; path ?: string ; error ?: string } > {
237- const sql = getDbClient ( ) ;
238-
239277 const sanitizedFolderName = folderName
240278 . replace ( / [ ^ a - z A - Z 0 - 9 - _ ] / g, "-" )
241279 . toLowerCase ( ) ;
@@ -244,22 +282,27 @@ export async function createMediaFolder(
244282 ? `${ parentFolder } /${ sanitizedFolderName } `
245283 : sanitizedFolderName ;
246284
285+ const placeholderPath = `${ folderPath } /.emptyFolderPlaceholder` ;
286+
247287 try {
248- const existing = await sql `
249- SELECT id FROM storage.objects
250- WHERE bucket_id = ${ BUCKET_NAME }
251- AND name LIKE ${ folderPath + "/%" }
252- LIMIT 1
253- ` ;
254-
255- 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 ) {
256293 return { success : false , error : "Folder already exists" } ;
257294 }
258295
259- await sql `
260- INSERT INTO storage.objects (bucket_id, name, owner, metadata)
261- VALUES (${ BUCKET_NAME } , ${ folderPath + "/.folder" } , NULL, '{"mimetype": "application/x-directory"}')
262- ` ;
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+ }
263306
264307 return {
265308 success : true ,
@@ -270,8 +313,6 @@ export async function createMediaFolder(
270313 success : false ,
271314 error : `Failed to create folder: ${ ( error as Error ) . message } ` ,
272315 } ;
273- } finally {
274- await sql . end ( ) ;
275316 }
276317}
277318
@@ -280,24 +321,31 @@ export async function moveMediaFile(
280321 fromPath : string ,
281322 toPath : string ,
282323) : Promise < { success : boolean ; newPath ?: string ; error ?: string } > {
283- const sql = getDbClient ( ) ;
284-
285324 try {
286- const filesInFolder = await sql `
287- SELECT name FROM storage.objects
288- WHERE bucket_id = ${ BUCKET_NAME }
289- AND name LIKE ${ fromPath + "/%" }
290- ` ;
325+ const { data : folderContents } = await supabase . storage
326+ . from ( BUCKET_NAME )
327+ . list ( fromPath , { limit : 1 } ) ;
291328
292- const isFolder = filesInFolder . length > 0 ;
329+ const isFolder = folderContents && folderContents . length > 0 ;
293330
294331 if ( isFolder ) {
295- await sql `
296- UPDATE storage.objects
297- SET name = ${ toPath } || SUBSTRING(name FROM ${ fromPath . length + 1 } )
298- WHERE bucket_id = ${ BUCKET_NAME }
299- AND name LIKE ${ fromPath + "/%" }
300- ` ;
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+ }
301349
302350 return {
303351 success : true ,
@@ -322,7 +370,5 @@ export async function moveMediaFile(
322370 success : false ,
323371 error : `Move failed: ${ ( error as Error ) . message } ` ,
324372 } ;
325- } finally {
326- await sql . end ( ) ;
327373 }
328374}
0 commit comments