1

Correction semi-automatique des liens morts

This commit is contained in:
2025-11-01 15:40:40 +01:00
parent f8b824c540
commit 890c95a450
36 changed files with 405 additions and 105 deletions

View File

@@ -0,0 +1,254 @@
const fs = require("fs");
const path = require("path");
const util = require("util");
const yaml = require("js-yaml");
const readline = require("readline");
const { execFile } = require("child_process");
const execFileAsync = util.promisify(execFile);
const SITE_ROOT = path.resolve(__dirname, "..");
const CONFIG_PATH = path.join(__dirname, "config.json");
let config = {};
if (fs.existsSync(CONFIG_PATH)) {
try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
} catch (error) {
console.warn(
`Impossible de parser ${path.relative(
SITE_ROOT,
CONFIG_PATH
)}. Valeurs par défaut utilisées. (${error.message})`
);
}
}
const externalConfig = {
cacheDir: path.join(__dirname, "cache"),
cacheFile: "external_links.yaml",
...(config.externalLinks || {}),
};
const CACHE_DIR = path.isAbsolute(externalConfig.cacheDir)
? externalConfig.cacheDir
: path.resolve(SITE_ROOT, externalConfig.cacheDir);
const CACHE_PATH = path.isAbsolute(externalConfig.cacheFile)
? externalConfig.cacheFile
: path.join(CACHE_DIR, externalConfig.cacheFile);
function ensureDirectoryExists(targetFile) {
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
}
function loadCache() {
if (!fs.existsSync(CACHE_PATH)) return {};
try {
return yaml.load(fs.readFileSync(CACHE_PATH, "utf8")) || {};
} catch (e) {
console.error("Erreur de lecture du cache YAML:", e.message);
return {};
}
}
function saveCache(cache) {
ensureDirectoryExists(CACHE_PATH);
fs.writeFileSync(CACHE_PATH, yaml.dump(cache), "utf8");
}
function promptFactory() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const question = (q) =>
new Promise((resolve) => rl.question(q, (ans) => resolve(ans.trim())));
return {
async ask(q) {
return await question(q);
},
close() {
rl.close();
},
};
}
async function ensureCheckRanIfNeeded() {
if (fs.existsSync(CACHE_PATH)) return;
console.log(
"Cache introuvable. Exécution préalable de tools/check_external_links.js..."
);
await execFileAsync("node", [path.join(__dirname, "check_external_links.js")], {
cwd: SITE_ROOT,
env: process.env,
});
}
function listBrokenUrls(cache) {
const result = [];
for (const [url, info] of Object.entries(cache)) {
const status = info && typeof info.status === "number" ? info.status : null;
const killed = info && info.manually_killed === true;
const validated = info && info.manually_validated === true;
if (killed) continue; // on ne traite plus ces URL
if (validated) continue; // déjà validé manuellement
if (status !== null && (status >= 400 || status === 0)) {
result.push({ url, info });
}
}
return result;
}
function getFilesForUrl(info) {
let files = [];
if (Array.isArray(info?.files) && info.files.length > 0) {
files = info.files;
} else if (Array.isArray(info?.locations) && info.locations.length > 0) {
files = Array.from(new Set(info.locations.map((s) => String(s).split(":")[0])));
}
return files.map((p) => path.resolve(SITE_ROOT, p));
}
function replaceInFile(filePath, from, to) {
if (!fs.existsSync(filePath)) return { changed: false };
const original = fs.readFileSync(filePath, "utf8");
if (!original.includes(from)) return { changed: false };
const updated = original.split(from).join(to);
if (updated !== original) {
fs.writeFileSync(filePath, updated, "utf8");
return { changed: true };
}
return { changed: false };
}
async function main() {
await ensureCheckRanIfNeeded();
let cache = loadCache();
const broken = listBrokenUrls(cache);
if (broken.length === 0) {
console.log("Aucun lien en erreur (>= 400) à traiter.");
return;
}
const p = promptFactory();
try {
for (const { url, info } of broken) {
const statusLabel = typeof info.status === "number" ? String(info.status) : "inconnu";
const locations = Array.isArray(info.locations) ? info.locations : [];
const files = Array.isArray(info.files) ? info.files : Array.from(new Set(locations.map((s) => String(s).split(":")[0])));
console.log("\nURL: ", url);
console.log("Statut: ", statusLabel);
if (locations.length > 0) {
console.log("Emplacements:");
for (const loc of locations) console.log(" - ", loc);
} else if (files.length > 0) {
console.log("Emplacements:");
for (const f of files) console.log(" - ", `${f}:?`);
} else {
console.log("Fichiers: (aucun chemin enregistré)");
}
const choice = (
await p.ask(
"Action ? [i]gnorer, [c]onfirmer, [r]emplacer, [m]ort, [q]uitter (défaut: i) : "
)
).toLowerCase() || "i";
if (choice === "q") {
console.log("Arrêt demandé.");
break;
}
if (choice === "i") {
// Ignorer
continue;
}
if (choice === "c") {
const nowIso = new Date().toISOString();
cache[url] = {
...(cache[url] || {}),
manually_validated: true,
manually_killed: cache[url]?.manually_killed === true,
status: 200,
errorType: null,
method: "MANUAL",
checked: nowIso,
};
saveCache(cache);
console.log("Marqué comme validé manuellement.");
continue;
}
if (choice === "m") {
cache[url] = {
...(cache[url] || {}),
manually_killed: true,
manually_validated: cache[url]?.manually_validated === true,
status: cache[url]?.status ?? null,
errorType: cache[url]?.errorType ?? null,
method: cache[url]?.method ?? null,
};
saveCache(cache);
console.log("Marqué comme mort (plus jamais retesté).");
continue;
}
if (choice === "r") {
if (!(Array.isArray(files) && files.length > 0)) {
console.log(
"Impossible de remplacer: aucun fichier enregistré pour cet URL. Relancez d'abord tools/check_external_links.js."
);
continue;
}
const newUrl = await p.ask("Nouvel URL: ");
if (!newUrl || !newUrl.includes("://")) {
console.log("URL invalide, opération annulée.");
continue;
}
// Remplacements dans les fichiers listés
let changedFiles = 0;
for (const rel of files) {
const abs = path.resolve(SITE_ROOT, rel);
const { changed } = replaceInFile(abs, url, newUrl);
if (changed) changedFiles++;
}
console.log(`Remplacements effectués dans ${changedFiles} fichier(s).`);
// Mettre à jour la base: déplacer l'entrée vers la nouvelle clé
const oldEntry = cache[url] || {};
const newEntryExisting = cache[newUrl] || {};
cache[newUrl] = {
...newEntryExisting,
files: Array.isArray(oldEntry.files) ? [...oldEntry.files] : files,
locations: Array.isArray(oldEntry.locations)
? [...oldEntry.locations]
: Array.isArray(oldEntry.files)
? oldEntry.files.map((f) => `${f}:?`)
: Array.isArray(locations)
? [...locations]
: [],
manually_validated: false,
manually_killed: false,
status: null,
errorType: null,
method: newEntryExisting.method || null,
checked: null,
};
delete cache[url];
saveCache(cache);
console.log("Base mise à jour pour le nouvel URL.");
continue;
}
console.log("Choix non reconnu. Ignoré.");
}
} finally {
p.close();
}
console.log("\nTerminé. Vous pouvez relancer 'node tools/check_external_links.js' pour mettre à jour les statuts.");
}
main().catch((err) => {
console.error("Erreur:", err);
process.exitCode = 1;
});