Plainleaf 自动发布 / release (push) Failing after 2m4s
Co-Authored-By: Codex <noreply@anthropic.com>
158 lines
4.5 KiB
TypeScript
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;
|
|
}
|