Grabber-In-Tanstack-Two / src /routes /api /download-zip.ts
ChBysk's picture
Upload folder using huggingface_hub
82e88b6 verified
Raw
History Blame Contribute Delete
5.2 kB
import { createFileRoute } from "@tanstack/react-router";
import { zipSync, strToU8 } from "fflate";
import { z } from "zod";
const PostSchema = z.object({
id: z.string(),
fileUrl: z.string().url(),
ext: z.string().default("jpg"),
previewUrl: z.string().optional(),
sampleUrl: z.string().optional(),
tags: z.array(z.string()).default([]),
source: z.string().optional(),
rating: z.string().optional(),
score: z.number().optional(),
fileSize: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
md5: z.string().optional(),
uploader: z.union([z.string(), z.number()]).optional(),
createdAt: z.string().optional(),
rawJson: z.string().optional(),
});
const BodySchema = z.object({
siteName: z.string().default("grabber"),
posts: z.array(PostSchema).min(1).max(200),
excludeTags: z.array(z.string()).default([]),
});
function sanitize(s: string) {
return s.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 60);
}
function humanSize(n?: number): string {
if (n == null || !Number.isFinite(n)) return "";
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(2)} MB`;
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
function buildMarkdown(post: z.infer<typeof PostSchema>, excludeSet: Set<string>): string {
const dims = post.width && post.height ? `${post.width} x ${post.height}` : "";
const rows: [string, string][] = [
["Source", post.source ?? ""],
["Rating", post.rating ?? ""],
["Score", post.score != null ? String(post.score) : ""],
["Dimensions", dims],
["File size", humanSize(post.fileSize)],
["MD5", post.md5 ?? ""],
["Uploader", post.uploader != null ? String(post.uploader) : ""],
["Created at", post.createdAt ?? ""],
["Original file", post.fileUrl],
["Sample", post.sampleUrl ?? ""],
["Preview", post.previewUrl ?? ""],
["Ext", post.ext],
];
const meta = rows.map(([k, v]) => `- **${k}:** ${v}`).join("\n");
const filteredTags = post.tags.filter((t) => !excludeSet.has(t.toLowerCase()));
const tags = filteredTags.length ? filteredTags.join(" ") : "";
let raw = "{}";
if (post.rawJson) {
try {
raw = JSON.stringify(JSON.parse(post.rawJson), null, 2);
} catch {
raw = post.rawJson;
}
}
return `# Post ${post.id}
${meta}
## Tags
${tags}
## Raw metadata
\`\`\`json
${raw}
\`\`\`
`;
}
export const Route = createFileRoute("/api/download-zip")({
server: {
handlers: {
POST: async ({ request }) => {
let body: z.infer<typeof BodySchema>;
try {
body = BodySchema.parse(await request.json());
} catch (err) {
return new Response(
JSON.stringify({ error: err instanceof Error ? err.message : "Invalid body" }),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
const files: Record<string, Uint8Array> = {};
const errors: string[] = [];
const siteName = sanitize(body.siteName);
const excludeSet = new Set((body.excludeTags || []).map((t) => t.toLowerCase()));
await Promise.all(
body.posts.map(async (post) => {
const idSafe = sanitize(post.id);
const extSafe = sanitize(post.ext || "jpg");
const base = `${siteName}_${idSafe}`;
// Filter out tags that user specified to be missing
const filteredTags = post.tags.filter((t) => !excludeSet.has(t.toLowerCase()));
// tags .txt — same basename as the image
files[`${base}.txt`] = strToU8(filteredTags.join(" "));
// per-post markdown metadata
files[`${idSafe}.md`] = strToU8(buildMarkdown(post, excludeSet));
// fetch the image itself
try {
const res = await fetch(post.fileUrl, {
headers: { "User-Agent": "LovableGrabber/1.0" },
});
if (!res.ok) {
errors.push(`${post.id}: HTTP ${res.status}`);
return;
}
const buf = new Uint8Array(await res.arrayBuffer());
files[`${base}.${extSafe}`] = buf;
} catch (err) {
errors.push(`${post.id}: ${err instanceof Error ? err.message : "fetch failed"}`);
}
}),
);
if (errors.length) {
files["_errors.txt"] = strToU8(errors.join("\n"));
}
if (Object.keys(files).length === 0) {
return new Response(JSON.stringify({ error: "No files downloaded" }), {
status: 502,
headers: { "Content-Type": "application/json" },
});
}
const zipped = zipSync(files, { level: 0 });
const ts = new Date().toISOString().replace(/[:.]/g, "-");
return new Response(zipped, {
status: 200,
headers: {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="grabber-${ts}.zip"`,
"Cache-Control": "no-store",
},
});
},
},
},
});