Bring over improvements from badmanners.xyz
This commit is contained in:
parent
d022fab5d6
commit
c55c82633d
24 changed files with 542 additions and 444 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { join, normalize as n } from "node:path/posix";
|
||||
import { program, Option } from "commander";
|
||||
|
||||
interface DeployLftpOptions {
|
||||
|
|
@ -18,8 +18,8 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
|
|||
"-c",
|
||||
[
|
||||
`open -u ${user},${password} ${host}`,
|
||||
`mirror --reverse --include-glob ${join(assetsFolder, "*")} --delete --only-missing --no-perms --verbose ${sourceFolder} ${targetFolder}`,
|
||||
`mirror --reverse --exclude-glob ${join(assetsFolder, "*")} --delete --no-perms --verbose ${sourceFolder} ${targetFolder}`,
|
||||
`mirror --reverse --include-glob ${join(assetsFolder, "*")} --delete --only-missing --no-perms --verbose ${n(sourceFolder)} ${n(targetFolder)}`,
|
||||
`mirror --reverse --exclude-glob ${join(assetsFolder, "*")} --delete --no-perms --verbose ${n(sourceFolder)} ${n(targetFolder)}`,
|
||||
`bye`,
|
||||
].join("\n"),
|
||||
],
|
||||
|
|
@ -28,13 +28,9 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
|
|||
},
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
process.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(`deploy-lftp failed with code ${code}`);
|
||||
}
|
||||
});
|
||||
process.on("close", (code) =>
|
||||
(code === 0) ? resolve() : reject(`lftp failed with code ${code}`),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { type ChildProcess, exec, spawnSync } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { readdir, mkdir, mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join as pathJoin, normalize } from "node:path";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { join, normalize } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { program } from "commander";
|
||||
import fetchRetryWrapper from "fetch-retry";
|
||||
|
||||
|
|
@ -31,26 +31,16 @@ function getRTFStyles(rtfSource: string) {
|
|||
|
||||
const fetchRetry = fetchRetryWrapper(global.fetch);
|
||||
|
||||
interface AstroApiResponse {
|
||||
story: string;
|
||||
description: Record<string, string>;
|
||||
thumbnail: string | null;
|
||||
}
|
||||
|
||||
const isLibreOfficeRunning = async () =>
|
||||
new Promise<boolean>((res, rej) => {
|
||||
exec("ps -ax", (err, stdout) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
new Promise<boolean>((res) => {
|
||||
spawn("ps", ["-ax"], {stdio: 'pipe'});
|
||||
const lines = createInterface({ input: spawn("ps", ["-ax"], {stdio: 'pipe'}).stdout });
|
||||
lines.on("line", (line) => {
|
||||
if (line.includes("libreoffice") && line.includes("--writer")) {
|
||||
res(true);
|
||||
}
|
||||
res(
|
||||
stdout
|
||||
.toLowerCase()
|
||||
.split("\n")
|
||||
.some((line) => line.includes("libreoffice") && line.includes("--writer")),
|
||||
);
|
||||
});
|
||||
lines.on("close", () => res(false));
|
||||
});
|
||||
|
||||
async function exportStory(slug: string, options: { outputDir: string }) {
|
||||
|
|
@ -72,53 +62,53 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
|||
console.error(`ERROR: Directory ${outputDir} is not empty!`);
|
||||
process.exit(1);
|
||||
}
|
||||
/* Check if Astro development server needs to be spawned */
|
||||
const healthcheckURL = `http://localhost:4321/api/healthcheck`;
|
||||
let devServerProcess: ChildProcess | null = null;
|
||||
|
||||
/* Spawn Astro dev server */
|
||||
console.log("Starting Astro development server...");
|
||||
const devServerProcess = spawn("./node_modules/.bin/astro", ["dev"], { stdio: 'pipe' });
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
const localServerRegex = /Local\s+(http\S+)/
|
||||
const lines = createInterface({ input: devServerProcess.stdout });
|
||||
lines.on("line", (line) => {
|
||||
const match = localServerRegex.exec(line);
|
||||
if (match && match[1]) {
|
||||
resolve(match[1]);
|
||||
}
|
||||
});
|
||||
lines.on("close", reject);
|
||||
});
|
||||
const astroURL = await promise;
|
||||
console.log(`Astro listening on ${astroURL}`);
|
||||
try {
|
||||
const response = await fetchRetry(healthcheckURL, { retries: 3, retryDelay: 1000 });
|
||||
const response = await fetchRetry(new URL(`/api/healthcheck`, astroURL), { retries: 5, retryDelay: 2000 });
|
||||
if (!response.ok) {
|
||||
throw new Error();
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const healthcheck = await response.json();
|
||||
if (!healthcheck.isAlive) {
|
||||
throw new Error();
|
||||
const healthcheck: { isAlive: boolean } = await response.json();
|
||||
if (healthcheck.isAlive !== true) {
|
||||
throw new Error(JSON.stringify(healthcheck));
|
||||
}
|
||||
} catch {
|
||||
/* Spawn Astro dev server */
|
||||
console.log("Starting Astro development server...");
|
||||
devServerProcess = exec("./node_modules/.bin/astro dev");
|
||||
await setTimeout(2000);
|
||||
try {
|
||||
const response = await fetchRetry(healthcheckURL, { retries: 5, retryDelay: 2000 });
|
||||
if (!response.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
const healthcheck = await response.json();
|
||||
if (!healthcheck.isAlive) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
console.error("ERROR: Astro dev server didn't respond in time!");
|
||||
devServerProcess && devServerProcess.kill();
|
||||
devServerProcess = null;
|
||||
process.exit(1);
|
||||
console.error("ERROR: Astro dev server didn't respond in time!");
|
||||
if (!devServerProcess.kill("SIGTERM")) {
|
||||
console.error("WARNING: Unable to shut down Astro dev server!");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/* Get data (story, thumbnail, descriptions) from Astro development server */
|
||||
let storyText = "";
|
||||
try {
|
||||
console.log("Getting data from Astro...");
|
||||
|
||||
const response = await fetch(`http://localhost:4321/api/export-story/${slug}`);
|
||||
const response = await fetch(new URL(`/api/export-story/${slug}`, astroURL));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to reach API (status code ${response.status})`);
|
||||
}
|
||||
const data: AstroApiResponse = await response.json();
|
||||
const data: { story: string, description: Record<string, string>, thumbnail: string | null } = await response.json();
|
||||
await Promise.all(
|
||||
Object.entries(data.description).map(async ([website, description]) => {
|
||||
return await writeFile(
|
||||
pathJoin(outputDir, `description_${website}.${website === "weasyl" ? "md" : "txt"}`),
|
||||
join(outputDir, `description_${website}.${website === "weasyl" ? "md" : "txt"}`),
|
||||
description,
|
||||
);
|
||||
}),
|
||||
|
|
@ -128,42 +118,41 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
|||
const thumbnailPath = data.thumbnail
|
||||
.replace(/^\/@fs/, "")
|
||||
.replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, "");
|
||||
await copyFile(thumbnailPath, pathJoin(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
|
||||
await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
|
||||
} else {
|
||||
const thumbnail = await fetch(data.thumbnail);
|
||||
if (!thumbnail.ok) {
|
||||
throw new Error("Failed to get thumbnail");
|
||||
}
|
||||
const thumbnailExt = thumbnail.headers.get("Content-Type")?.startsWith("image/png") ? "png" : "jpg";
|
||||
await writeFile(pathJoin(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
||||
await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
||||
}
|
||||
}
|
||||
storyText = data.story;
|
||||
writeFile(pathJoin(outputDir, `${slug}.txt`), storyText);
|
||||
writeFile(join(outputDir, `${slug}.txt`), storyText);
|
||||
} finally {
|
||||
if (devServerProcess) {
|
||||
console.log("Shutting down the Astro development server...");
|
||||
if (!devServerProcess.kill("SIGTERM")) {
|
||||
console.error("WARNING: Unable to shut down Astro dev server!");
|
||||
}
|
||||
devServerProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse story into output formats */
|
||||
console.log("Parsing story into output formats...");
|
||||
await writeFile(pathJoin(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
||||
const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
|
||||
await writeFile(pathJoin(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
||||
await writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "export-story-"));
|
||||
await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
||||
spawnSync(
|
||||
"libreoffice",
|
||||
["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, pathJoin(tempDir, "temp.txt")],
|
||||
["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
|
||||
const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
|
||||
const rtfStyles = getRTFStyles(rtfText);
|
||||
await writeFile(
|
||||
pathJoin(outputDir, `${slug}.rtf`),
|
||||
join(outputDir, `${slug}.rtf`),
|
||||
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
||||
);
|
||||
console.log("Success!");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue