Skip to content
45 linesCodeBlameRaw
1import { data } from "react-router";
2
3import type { Route } from "./+types/export";
4import { docs } from "../../../lib/services.server";
5import { requireUser, roleIn } from "../../../lib/session.server";
6import { zip } from "../../../lib/zip";
7
8/**
9 * Docs as Markdown files: `?page=<id>` downloads a page as `<title>.md`;
10 * `?space=<id>` downloads a space as a zip of its pages, in folders that
11 * follow its tree. Only what the viewer can read.
12 */
13export async function loader({ params, context, request }: Route.LoaderArgs) {
14 const viewer = requireUser(context, request);
15 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
16 const slug = params.owner.toLowerCase();
17 const url = new URL(request.url);
18 const page = url.searchParams.get("page");
19 const space = url.searchParams.get("space");
20 if (page) {
21 const found = await docs.exportPage(slug, page, viewer);
22 if (!found.ok) throw data(found.error.message, { status: found.error.code === "not_found" ? 404 : 403 });
23 return new Response(found.value.markdown, {
24 headers: {
25 "content-type": "text/markdown; charset=utf-8",
26 "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(found.value.filename)}`,
27 "cache-control": "no-store",
28 },
29 });
30 }
31 if (space) {
32 const found = await docs.exportSpace(slug, space, viewer);
33 if (!found.ok) throw data(found.error.message, { status: found.error.code === "not_found" ? 404 : 403 });
34 const encoder = new TextEncoder();
35 const bytes = await zip(found.value.files.map((f) => ({ path: `${found.value.name}/${f.path}`, data: encoder.encode(f.markdown) })));
36 return new Response(bytes, {
37 headers: {
38 "content-type": "application/zip",
39 "content-disposition": `attachment; filename="${found.value.name}.zip"`,
40 "cache-control": "no-store",
41 },
42 });
43 }
44 throw data("Which page or space?", { status: 400 });
45}