tfrere's picture
tfrere HF Staff
feat(storage): first-class data - no silent failures in the persistence pipeline
7a42df5
Raw
History Blame Contribute Delete
3.12 kB
import { Router, type Request, type Response } from "express";
import { createReadStream, existsSync } from "fs";
import { extractToken, resolveUser } from "../auth.js";
import { getStorageStatus } from "../hf-storage.js";
import { docPath, sanitizeName } from "../utils.js";
interface StorageContext {
oauthEnabled: boolean;
}
/**
* Routes that surface the state of the persistence pipeline and
* provide manual escape hatches for disaster recovery.
*
* GET /api/storage/status
* Returns the current pipeline state (datasetReady,
* lastLocalSaveAt, lastCloudPushAt, pendingPush, lastError).
* Polled by the SyncIndicator every few seconds. Auth-gated to
* canEdit so we don't leak error details (which may include
* dataset ids) to anonymous viewers.
*
* GET /api/admin/export-doc?name=<docName>
* Streams the raw `.yjs` file off disk so an editor can
* download a snapshot manually. The cardinal use case is
* "the cloud push has been failing for hours, the container
* is about to rebuild, I need to get my data out NOW". Same
* canEdit gate.
*/
export function createStorageRouter(ctx: StorageContext): Router {
const router = Router();
router.get("/api/storage/status", async (req, res) => {
if (ctx.oauthEnabled) {
const token = extractToken(req.headers.cookie);
const user = await resolveUser(token);
if (!user || !user.canEdit) {
res.status(403).json({ error: "Unauthorized" });
return;
}
}
res.json(getStorageStatus());
});
router.get("/api/admin/export-doc", async (req: Request, res: Response) => {
if (ctx.oauthEnabled) {
const token = extractToken(req.headers.cookie);
const user = await resolveUser(token);
if (!user || !user.canEdit) {
res.status(403).json({ error: "Unauthorized" });
return;
}
}
// `name` is the doc id; default to the only doc the editor
// currently supports ("default"). Sanitised before touching FS.
const rawName = typeof req.query.name === "string" ? req.query.name : "default";
const safeName = sanitizeName(rawName);
const path = docPath(rawName);
if (!existsSync(path)) {
res.status(404).json({ error: "No on-disk snapshot for that doc" });
return;
}
// Force a download with a date-stamped filename so multiple
// exports don't overwrite each other in the user's downloads
// folder. ISO date without colons (Windows / macOS Downloads
// are happier without them).
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader(
"Content-Disposition",
`attachment; filename="${safeName}-${stamp}.yjs"`,
);
res.setHeader("Cache-Control", "no-store");
const stream = createReadStream(path);
stream.on("error", (err) => {
console.error(`[admin-export] stream error for ${safeName}:`, err.message);
if (!res.headersSent) res.status(500).end();
else res.end();
});
stream.pipe(res);
});
return router;
}