feat: nahravanie obrazkov podla obsahu - rovnaky subor sa neulozi druhykrat
Nazov suboru zacina odtlackom obsahu (SHA-256); ak rovnaky subor uz v ulozisku je, pouzije sa jeho adresa. Opakovane importy ZIP-u uz nevytvaraju duplikaty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
81802ab001
commit
66c098190d
2 files changed
+32
-3
No files matched your search
@@ -5,18 +5,35 @@ const BUCKET = 'newsletter-assets';
|
||||
|
||||
// Vydá podpísanú adresu na nahratie súboru priamo z prehliadača do Supabase Storage.
|
||||
// Samotný súbor cez server nejde – Vercel by odmietol požiadavky nad 4,5 MB („Request Entity Too Large“).
|
||||
// Názov súboru začína odtlačkom obsahu (hash): ak rovnaký súbor už v úložisku je, vráti sa jeho adresa
|
||||
// a nič sa nenahráva – opakované importy tak nevytvárajú duplikáty.
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { name } = (await req.json()) as { name?: string };
|
||||
const { name, hash } = (await req.json()) as { name?: string; hash?: string };
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: 'Chýba názov súboru.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const admin = getSupabaseAdmin();
|
||||
const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const path = `uploads/${Date.now()}-${safeName}`;
|
||||
const contentHash = hash && /^[a-f0-9]{16,64}$/.test(hash) ? hash : null;
|
||||
|
||||
if (contentHash) {
|
||||
const { data: found } = await admin.storage.from(BUCKET).list('uploads', { search: contentHash, limit: 1 });
|
||||
const existing = found?.find((f) => f.name.startsWith(`${contentHash}-`));
|
||||
if (existing) {
|
||||
const { data: pub } = admin.storage.from(BUCKET).getPublicUrl(`uploads/${existing.name}`);
|
||||
return NextResponse.json({ success: true, exists: true, url: pub.publicUrl });
|
||||
}
|
||||
}
|
||||
|
||||
const path = `uploads/${contentHash || Date.now()}-${safeName}`;
|
||||
const { data, error } = await admin.storage.from(BUCKET).createSignedUploadUrl(path);
|
||||
if (error && contentHash && /exist/i.test(error.message)) {
|
||||
// ten istý súbor práve nahral niekto iný
|
||||
const { data: pub } = admin.storage.from(BUCKET).getPublicUrl(path);
|
||||
return NextResponse.json({ success: true, exists: true, url: pub.publicUrl });
|
||||
}
|
||||
if (error || !data) {
|
||||
console.error('Signed upload URL error:', error);
|
||||
return NextResponse.json({ error: error?.message || 'Nepodarilo sa pripraviť nahratie' }, { status: 500 });
|
||||
|
||||
+13
-1
@@ -2,13 +2,25 @@
|
||||
// (cez Vercel by väčšie súbory neprešli – limit 4,5 MB na požiadavku).
|
||||
import { supabase } from './supabase';
|
||||
|
||||
// Odtlačok obsahu súboru (prvých 24 znakov SHA-256) – rovnaký obrázok = rovnaký názov v úložisku
|
||||
async function contentHash(file: Blob): Promise<string | undefined> {
|
||||
try {
|
||||
const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer());
|
||||
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, 24);
|
||||
} catch {
|
||||
return undefined; // bez odtlačku sa súbor nahrá pod časovou pečiatkou
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFile(file: Blob, name: string): Promise<string> {
|
||||
const hash = await contentHash(file);
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
body: JSON.stringify({ name, hash })
|
||||
});
|
||||
const data = await res.json().catch(() => ({ error: `Server vrátil chybu ${res.status}` }));
|
||||
if (data.success && data.exists && data.url) return data.url; // rovnaký súbor už v úložisku je
|
||||
if (!data.success || !data.path || !data.token) {
|
||||
throw new Error(data.error || 'Nahratie súboru zlyhalo');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user