Files
wushenghuaandCodex 9fa3727cb1
Plainleaf 自动发布 / release (push) Failing after 2m4s
feat: 增加无需登录的笔记分享
Co-Authored-By: Codex <noreply@anthropic.com>
2026-08-13 12:58:22 +08:00

158 lines
4.5 KiB
TypeScript

import { strFromU8, strToU8 } from "fflate";
import type { SpacePrimitives } from "../spaces/space_primitives.ts";
export const plainleafShareRoot = ".plainleaf/shares";
export type PlainleafShareFormat = "markdown" | "html";
export type PlainleafShare = {
token: string;
path: string;
title: string;
format: PlainleafShareFormat;
createdAt: string;
};
function validToken(token: string): boolean {
return /^[a-f0-9]{32}$/.test(token);
}
function validSharedPath(path: string, format: PlainleafShareFormat): boolean {
const parts = path.split("/");
return (
!!path &&
!path.startsWith(".") &&
!path.startsWith("/") &&
!path.includes("\\") &&
!path.includes("\0") &&
parts.every((part) => !!part && part !== "." && part !== "..") &&
(format === "markdown"
? path.toLocaleLowerCase().endsWith(".md")
: path.toLocaleLowerCase().endsWith(".html"))
);
}
function manifestPath(token: string): string {
return `${plainleafShareRoot}/${token}.json`;
}
function parseShare(data: Uint8Array): PlainleafShare | undefined {
try {
const value = JSON.parse(strFromU8(data)) as Partial<PlainleafShare>;
if (
typeof value.token !== "string" ||
typeof value.path !== "string" ||
typeof value.title !== "string" ||
typeof value.createdAt !== "string" ||
(value.format !== "markdown" && value.format !== "html") ||
!validToken(value.token) ||
!validSharedPath(value.path, value.format)
) {
return undefined;
}
return value as PlainleafShare;
} catch {
return undefined;
}
}
export async function listPlainleafShares(
storage: SpacePrimitives,
): Promise<PlainleafShare[]> {
const manifests = (await storage.fetchFileList()).filter(
(file) =>
file.name.startsWith(`${plainleafShareRoot}/`) &&
file.name.endsWith(".json"),
);
const shares = await Promise.all(
manifests.map(async (manifest) => {
try {
return parseShare((await storage.readFile(manifest.name)).data);
} catch {
return undefined;
}
}),
);
return shares
.filter((share): share is PlainleafShare => !!share)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
export async function findPlainleafShare(
storage: SpacePrimitives,
path: string,
): Promise<PlainleafShare | undefined> {
return (await listPlainleafShares(storage)).find(
(share) => share.path === path,
);
}
export async function createPlainleafShare(
storage: SpacePrimitives,
input: { path: string; title: string; format: PlainleafShareFormat },
now = new Date(),
token = crypto.randomUUID().replaceAll("-", ""),
): Promise<PlainleafShare> {
if (!validToken(token) || !validSharedPath(input.path, input.format)) {
throw new Error("无法为这个文件创建安全的分享链接");
}
const existing = await findPlainleafShare(storage, input.path);
if (existing) return existing;
const share: PlainleafShare = {
...input,
token,
title: input.title.trim() || "未命名笔记",
createdAt: now.toISOString(),
};
await storage.writeFile(
manifestPath(token),
strToU8(JSON.stringify(share, null, 2)),
);
return share;
}
export async function deletePlainleafShare(
storage: SpacePrimitives,
share: PlainleafShare,
): Promise<void> {
await storage.deleteFile(manifestPath(share.token));
}
export async function deletePlainleafSharesForPaths(
storage: SpacePrimitives,
paths: string[],
): Promise<number> {
const targets = new Set(paths);
const shares = (await listPlainleafShares(storage)).filter((share) =>
targets.has(share.path),
);
for (const share of shares) await deletePlainleafShare(storage, share);
return shares.length;
}
export async function movePlainleafShares(
storage: SpacePrimitives,
moves: Array<{ from: string; to: string; title?: string }>,
): Promise<number> {
const bySource = new Map(moves.map((move) => [move.from, move]));
const shares = (await listPlainleafShares(storage)).filter((share) =>
bySource.has(share.path),
);
for (const share of shares) {
const move = bySource.get(share.path)!;
const next: PlainleafShare = {
...share,
path: move.to,
title: move.title?.trim() || share.title,
};
if (!validSharedPath(next.path, next.format)) {
throw new Error("分享链接无法跟随到新的文件路径");
}
await storage.writeFile(
manifestPath(share.token),
strToU8(JSON.stringify(next, null, 2)),
);
}
return shares.length;
}