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.
|
// 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“).
|
// 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) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { name } = (await req.json()) as { name?: string };
|
const { name, hash } = (await req.json()) as { name?: string; hash?: string };
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return NextResponse.json({ error: 'Chýba názov súboru.' }, { status: 400 });
|
return NextResponse.json({ error: 'Chýba názov súboru.' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const admin = getSupabaseAdmin();
|
const admin = getSupabaseAdmin();
|
||||||
const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
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);
|
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) {
|
if (error || !data) {
|
||||||
console.error('Signed upload URL error:', error);
|
console.error('Signed upload URL error:', error);
|
||||||
return NextResponse.json({ error: error?.message || 'Nepodarilo sa pripraviť nahratie' }, { status: 500 });
|
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).
|
// (cez Vercel by väčšie súbory neprešli – limit 4,5 MB na požiadavku).
|
||||||
import { supabase } from './supabase';
|
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> {
|
export async function uploadFile(file: Blob, name: string): Promise<string> {
|
||||||
|
const hash = await contentHash(file);
|
||||||
const res = await fetch('/api/upload', {
|
const res = await fetch('/api/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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}` }));
|
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) {
|
if (!data.success || !data.path || !data.token) {
|
||||||
throw new Error(data.error || 'Nahratie súboru zlyhalo');
|
throw new Error(data.error || 'Nahratie súboru zlyhalo');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user