Correction semi-automatique des liens morts
This commit is contained in:
254
tools/update_external_links.js
Normal file
254
tools/update_external_links.js
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user