222 lines
6.6 KiB
JavaScript
222 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { Pool } = require("pg");
|
|
const { loadEnv } = require("./lib/env");
|
|
const { loadToolsConfig } = require("./lib/config");
|
|
const { readFrontmatterFile } = require("./lib/frontmatter");
|
|
const {
|
|
resolveBundlePath,
|
|
ensureBundleExists,
|
|
ensureWithinContent,
|
|
splitRelativeParts,
|
|
resolveDestination,
|
|
moveBundle,
|
|
addAlias,
|
|
cleanupEmptyParents,
|
|
findDateSegments,
|
|
} = require("./lib/article_move");
|
|
const {
|
|
normalizeLemmyConfig,
|
|
createLemmyClient,
|
|
buildArticleUrl,
|
|
buildCommunityDescriptor,
|
|
ensureCommunity,
|
|
isYearSegment,
|
|
isMonthSegment,
|
|
isDaySegment,
|
|
} = require("./lib/lemmy");
|
|
|
|
const CONTENT_ROOT = path.join(__dirname, "..", "content");
|
|
const FRONTMATTER_COMMENT_FIELD = "comments_url";
|
|
const DEFAULT_DATABASE_URL = "postgres:///lemmy?host=/run/postgresql&user=richard";
|
|
main().then(
|
|
() => {
|
|
process.exit(0);
|
|
},
|
|
(error) => {
|
|
console.error(`❌ Déplacement interrompu : ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Point d'entrée : déplace le bundle et synchronise Lemmy si possible.
|
|
*/
|
|
async function main() {
|
|
loadEnv();
|
|
const args = process.argv.slice(2);
|
|
if (args.length < 2) {
|
|
throw new Error("Usage: node tools/move_article.js <chemin_source> <chemin_destination>");
|
|
}
|
|
|
|
const sourceInput = args[0];
|
|
const destinationInput = args[1];
|
|
|
|
const sourceBundle = resolveBundlePath(sourceInput);
|
|
ensureBundleExists(sourceBundle);
|
|
ensureWithinContent(sourceBundle, CONTENT_ROOT);
|
|
|
|
const sourceRelativeParts = splitRelativeParts(sourceBundle, CONTENT_ROOT);
|
|
const sourceSlug = sourceRelativeParts[sourceRelativeParts.length - 1];
|
|
const sourceDate = findDateSegments(sourceRelativeParts.slice(0, -1), isDateSegment);
|
|
|
|
const destination = resolveDestination(
|
|
destinationInput,
|
|
sourceSlug,
|
|
sourceDate,
|
|
CONTENT_ROOT,
|
|
isDateSegment
|
|
);
|
|
ensureWithinContent(destination.bundleDir, CONTENT_ROOT);
|
|
|
|
if (fs.existsSync(destination.bundleDir)) {
|
|
throw new Error(`Le bundle ${destination.bundleDir} existe déjà.`);
|
|
}
|
|
|
|
const sourceFrontmatter = readFrontmatterFile(path.join(sourceBundle, "index.md"));
|
|
if (!sourceFrontmatter) {
|
|
throw new Error(`Frontmatter introuvable pour ${sourceBundle}.`);
|
|
}
|
|
|
|
moveBundle(sourceBundle, destination.bundleDir);
|
|
addAlias(destination.bundleDir, sourceRelativeParts);
|
|
cleanupEmptyParents(path.dirname(sourceBundle), CONTENT_ROOT);
|
|
|
|
await updateLemmyIfNeeded(sourceFrontmatter.data, destination.bundleDir);
|
|
}
|
|
|
|
/**
|
|
* Met à jour Lemmy si un comments_url est présent.
|
|
* @param {object} frontmatterData Données du frontmatter.
|
|
* @param {string} bundleDir Chemin du bundle après déplacement.
|
|
*/
|
|
async function updateLemmyIfNeeded(frontmatterData, bundleDir) {
|
|
const commentsUrl = extractCommentsUrl(frontmatterData);
|
|
if (!commentsUrl) {
|
|
return;
|
|
}
|
|
|
|
const postId = extractPostId(commentsUrl);
|
|
if (!postId) {
|
|
console.warn("⚠️ comments_url invalide, mise à jour Lemmy ignorée.");
|
|
return;
|
|
}
|
|
|
|
const toolsConfig = await loadToolsConfig(path.join(__dirname, "config", "config.json"));
|
|
const lemmyConfig = normalizeLemmyConfig(toolsConfig.lemmy);
|
|
const client = await createLemmyClient(lemmyConfig);
|
|
const databaseUrl = resolveDatabaseUrl();
|
|
const pool = new Pool({ connectionString: databaseUrl });
|
|
|
|
const bundleParts = splitRelativeParts(bundleDir, CONTENT_ROOT);
|
|
const descriptor = buildCommunityDescriptor(bundleParts, lemmyConfig.community);
|
|
const community = await ensureCommunity(client, descriptor, lemmyConfig.community);
|
|
const communityId = community.id;
|
|
await updatePostForMove(pool, postId, communityId, buildArticleUrl(lemmyConfig.siteUrl, bundleParts));
|
|
|
|
await pool.end();
|
|
}
|
|
|
|
/**
|
|
* Extrait une URL de commentaires depuis les données du frontmatter.
|
|
* @param {object} frontmatterData Données du frontmatter.
|
|
* @returns {string} URL des commentaires ou chaîne vide.
|
|
*/
|
|
function extractCommentsUrl(frontmatterData) {
|
|
if (!frontmatterData) {
|
|
return "";
|
|
}
|
|
if (typeof frontmatterData[FRONTMATTER_COMMENT_FIELD] !== "string") {
|
|
return "";
|
|
}
|
|
return frontmatterData[FRONTMATTER_COMMENT_FIELD].trim();
|
|
}
|
|
|
|
/**
|
|
* Extrait l'identifiant numérique d'un comments_url Lemmy.
|
|
* @param {string} url URL issue du frontmatter.
|
|
* @returns {number|null} Identifiant ou null si non reconnu.
|
|
*/
|
|
function extractPostId(url) {
|
|
const trimmed = url.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const normalized = trimmed.replace(/\/+$/, "");
|
|
const match = normalized.match(/\/(?:post|c\/[^/]+\/post)\/(\d+)(?:$|\?)/i);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return Number.parseInt(match[1], 10);
|
|
}
|
|
|
|
/**
|
|
* Met à jour le post Lemmy après déplacement.
|
|
* @param {Pool} pool Pool Postgres.
|
|
* @param {number} postId Identifiant du post.
|
|
* @param {number} communityId Communauté cible.
|
|
* @param {string} newUrl Nouvelle URL Hugo.
|
|
*/
|
|
async function updatePostForMove(pool, postId, communityId, newUrl) {
|
|
await pool.query("update post set community_id = $1, url = $2 where id = $3", [
|
|
communityId,
|
|
newUrl,
|
|
postId,
|
|
]);
|
|
|
|
const hasAggregates = await tableHasColumn(pool, "post_aggregates", "community_id");
|
|
if (hasAggregates) {
|
|
await pool.query("update post_aggregates set community_id = $1 where post_id = $2", [
|
|
communityId,
|
|
postId,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indique si une table possède une colonne donnée.
|
|
* @param {Pool} pool Pool Postgres.
|
|
* @param {string} tableName Nom de la table.
|
|
* @param {string} columnName Nom de la colonne.
|
|
* @returns {Promise<boolean>} true si la colonne existe.
|
|
*/
|
|
async function tableHasColumn(pool, tableName, columnName) {
|
|
const result = await pool.query(
|
|
"select column_name from information_schema.columns where table_name = $1 and column_name = $2 limit 1",
|
|
[tableName, columnName]
|
|
);
|
|
return result.rowCount === 1;
|
|
}
|
|
|
|
/**
|
|
* Résout l'URL de connexion Postgres pour Lemmy.
|
|
* @returns {string} URL de connexion.
|
|
*/
|
|
function resolveDatabaseUrl() {
|
|
if (typeof process.env.LEMMY_DATABASE_URL === "string" && process.env.LEMMY_DATABASE_URL.trim()) {
|
|
return process.env.LEMMY_DATABASE_URL.trim();
|
|
}
|
|
return DEFAULT_DATABASE_URL;
|
|
}
|
|
|
|
/**
|
|
* Détermine si des segments forment une date au format année/mois/jour.
|
|
* @param {string[]} parts Segments du chemin.
|
|
* @param {number} index Position de départ.
|
|
* @returns {string[]|null} Segments de date ou null.
|
|
*/
|
|
function isDateSegment(parts, index) {
|
|
if (!isYearSegment(parts[index])) {
|
|
return null;
|
|
}
|
|
if (!isMonthSegment(parts[index + 1])) {
|
|
return null;
|
|
}
|
|
if (!isDaySegment(parts[index + 2])) {
|
|
return null;
|
|
}
|
|
return [parts[index], parts[index + 1], parts[index + 2]];
|
|
}
|