Script pour déplacer des articles
This commit is contained in:
221
tools/move_article.js
Normal file
221
tools/move_article.js
Normal 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]];
|
||||
}
|
||||
Reference in New Issue
Block a user