Skip to content

Commit e82b92c

Browse files
refactor(media): replace postgres direct query with supabase storage methods
1 parent 4e87265 commit e82b92c

File tree

1 file changed

+86
-55
lines changed

1 file changed

+86
-55
lines changed

apps/web/src/functions/supabase-media.ts

Lines changed: 86 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import type { SupabaseClient } from "@supabase/supabase-js";
22
import { createClient } from "@supabase/supabase-js";
3-
import postgres from "postgres";
43

54
import { env } from "@/env";
65

76
const BUCKET_NAME = "blog";
87

9-
function getDbClient() {
10-
return postgres(env.DATABASE_URL, { prepare: false });
11-
}
12-
138
export 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+
188210
export 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

247272
export 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-zA-Z0-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

Comments
 (0)