Files
MineGeo/lib/map-transfer.ts

134 lines
4.5 KiB
TypeScript

import { api } from "./client-api";
import type { ArchiveMetadata } from "../shared/map-archive";
type Progress = (message: string, values?: Record<string, number>) => 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<string, Uint8Array> = 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;
}
}