import { api } from "./client-api"; import type { ArchiveMetadata } from "../shared/map-archive"; type Progress = (message: string, values?: Record) => void; export async function exportMap(id: string, progress: Progress) { const { archiveMetadataSchema, archiveSchema, archiveLimit, archiveFileLimit, archiveManifestName, sha256, } = await import("../shared/map-archive"); const { zipSync, strToU8 } = await import("fflate"); const result = await api<{ manifest: ArchiveMetadata; assets: { path: string; url: string }[]; }>(`datasets/${id}/export`); const metadata = archiveMetadataSchema.parse(result.manifest); const files: Record = Object.create(null); const assets = []; let total = 0; for (const [index, asset] of result.assets.entries()) { progress("Exporting images: {done} / {total}", { done: index + 1, total: result.assets.length, }); const response = await fetch(asset.url, { signal: AbortSignal.timeout(60000), cache: "no-store", }); if (!response.ok || !response.body) throw new Error("Could not download map images. Please try again."); const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0; while (true) { const { value, done } = await reader.read(); if (done) break; size += value.length; total += value.length; if (size > archiveFileLimit || total > archiveLimit - 128 * 1024) { await reader.cancel(); throw new Error("Map archive exceeds the 256 MB limit"); } chunks.push(value); } const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; } files[asset.path] = bytes; assets.push({ path: asset.path, bytes: size, sha256: await sha256(bytes) }); } const archive = archiveSchema.parse({ ...metadata, assets }); files[archiveManifestName] = strToU8(JSON.stringify(archive)); progress("Preparing ZIP…"); const zip = zipSync(files, { level: 0 }); if (zip.length > archiveLimit) throw new Error("Map archive exceeds the 256 MB limit"); const url = URL.createObjectURL( new Blob([new Uint8Array(zip).buffer], { type: "application/zip" }), ); const link = document.createElement("a"); link.href = url; link.download = `${metadata.name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80) || "minegeo-map"}.zip`; document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 60000); } export async function importMap(file: File, progress: Progress) { const { archiveLimit, readMapArchive } = await import( "../shared/map-archive" ); if (!file.name.toLowerCase().endsWith(".zip")) throw new Error("Choose a MineGeo ZIP file"); if (file.size > archiveLimit) throw new Error("Map archive exceeds the 256 MB limit"); progress("Checking map ZIP…"); const { archive, files } = await readMapArchive( new Uint8Array(await file.arrayBuffer()), ); const { datasetId } = await api<{ datasetId: string }>("uploads/import", { manifest: archive, }); const signed = await api<{ assets: { path: string; objectName: string; token: string; apiKey: string; endpoint: string; }[]; }>("uploads/sign", { datasetId }); const { createBrowserClient } = await import("@supabase/ssr"); const client = createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, ); try { for (const [index, asset] of signed.assets.entries()) { progress("Importing images: {done} / {total}", { done: index + 1, total: signed.assets.length, }); const content = files.get(asset.path); if (!content) throw new Error("Invalid or incomplete map ZIP"); const { error } = await client.storage .from("minegeo-raw") .uploadToSignedUrl( asset.objectName, asset.token, new Blob([new Uint8Array(content).buffer], { type: "image/webp" }), { contentType: "image/webp" }, ); if (error) throw new Error( "Map upload failed. Check your connection and import the ZIP again.", ); } progress("Preparing your private draft…"); await api("uploads/finalize", { datasetId }); return datasetId; } catch (error) { await api(`datasets/${datasetId}/cancel`, {}).catch(() => {}); throw error; } }