255 lines
7.8 KiB
JavaScript
255 lines
7.8 KiB
JavaScript
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;
|
|
});
|