1

Script pour déplacer des articles

This commit is contained in:
2025-12-22 00:38:55 +01:00
parent 08dd21653f
commit 7b1aeb78a3
5 changed files with 829 additions and 2 deletions

221
tools/move_article.js Normal file
View File

@@ -0,0 +1,221 @@
#!/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]];
}