gallery_server_client_s190js/tools/cleanupCast.js
2026-10-26 11:00:14 +01:00

463 lines
7.5 KiB
JavaScript

// tools/cleanupCast.js
const fs = require('fs');
const path = require('path');
const CAST_ROOT = path.join(
process.cwd(),
'public',
'cast'
);
const MIN_AGE_MS =
24 * 60 * 60 * 1000;
function getAgeMs(stat, now) {
return now - stat.mtimeMs;
}
function formatAge(ageMs) {
return (
ageMs /
(60 * 60 * 1000)
).toFixed(2);
}
/*
* ============================================================
* FOTO CAST
* ============================================================
*
* Ricerca ricorsivamente sotto:
*
* public/cast/<user>/photos/
*
* e cancella soltanto file con almeno 24 ore.
*/
async function cleanupPhotos({
photosDir,
dryRun,
now,
counters,
}) {
let entries;
try {
entries = await fs.promises.readdir(
photosDir,
{
withFileTypes: true,
},
);
} catch (error) {
if (error.code === 'ENOENT') {
return;
}
throw error;
}
for (const entry of entries) {
const fullPath = path.join(
photosDir,
entry.name,
);
if (entry.isDirectory()) {
await cleanupPhotos({
photosDir: fullPath,
dryRun,
now,
counters,
});
/*
* Rimuove eventuali directory vuote.
*/
try {
const remaining =
await fs.promises.readdir(fullPath);
if (remaining.length === 0) {
if (dryRun) {
console.log(
'[cleanupCast] WOULD REMOVE EMPTY DIR',
fullPath,
);
} else {
await fs.promises.rmdir(fullPath);
console.log(
'[cleanupCast] removed empty dir',
fullPath,
);
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
continue;
}
if (!entry.isFile()) {
continue;
}
const stat =
await fs.promises.stat(fullPath);
const ageMs =
getAgeMs(stat, now);
counters.photoFilesChecked += 1;
if (ageMs < MIN_AGE_MS) {
console.log(
'[cleanupCast] KEEP photo',
`age=${formatAge(ageMs)}h`,
fullPath,
);
counters.kept += 1;
continue;
}
if (dryRun) {
console.log(
'[cleanupCast] WOULD DELETE photo',
`age=${formatAge(ageMs)}h`,
fullPath,
);
} else {
await fs.promises.unlink(fullPath);
console.log(
'[cleanupCast] deleted photo',
`age=${formatAge(ageMs)}h`,
fullPath,
);
}
counters.deleted += 1;
}
}
/*
* ============================================================
* VIDEO CAST
* ============================================================
*
* Determina la data piu recente di tutti i file presenti
* nella directory HLS.
*
* In questo modo un video viene eliminato soltanto quando
* tutti i file che lo compongono hanno almeno 24 ore.
*/
async function getNewestMtimeMs(dir) {
const entries =
await fs.promises.readdir(
dir,
{
withFileTypes: true,
},
);
let newest = 0;
for (const entry of entries) {
const fullPath =
path.join(dir, entry.name);
if (entry.isDirectory()) {
const childNewest =
await getNewestMtimeMs(fullPath);
newest =
Math.max(
newest,
childNewest,
);
continue;
}
if (!entry.isFile()) {
continue;
}
const stat =
await fs.promises.stat(fullPath);
newest =
Math.max(
newest,
stat.mtimeMs,
);
}
return newest;
}
/*
* Identifica le directory che rappresentano
* effettivamente un video HLS cercando master.m3u8.
*/
async function findVideoDirs(
videosRoot,
result = [],
) {
let entries;
try {
entries =
await fs.promises.readdir(
videosRoot,
{
withFileTypes: true,
},
);
} catch (error) {
if (error.code === 'ENOENT') {
return result;
}
throw error;
}
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const fullPath =
path.join(
videosRoot,
entry.name,
);
const masterPath =
path.join(
fullPath,
'master.m3u8',
);
try {
const stat =
await fs.promises.stat(
masterPath,
);
if (stat.isFile()) {
result.push(fullPath);
continue;
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
await findVideoDirs(
fullPath,
result,
);
}
return result;
}
async function cleanupVideos({
videosRoot,
dryRun,
now,
counters,
}) {
const videoDirs =
await findVideoDirs(
videosRoot,
);
for (const videoDir of videoDirs) {
counters.videoDirsChecked += 1;
const newestMtimeMs =
await getNewestMtimeMs(
videoDir,
);
if (!newestMtimeMs) {
continue;
}
const ageMs =
now - newestMtimeMs;
if (ageMs < MIN_AGE_MS) {
console.log(
'[cleanupCast] KEEP video',
`age=${formatAge(ageMs)}h`,
videoDir,
);
counters.kept += 1;
continue;
}
if (dryRun) {
console.log(
'[cleanupCast] WOULD DELETE video',
`age=${formatAge(ageMs)}h`,
videoDir,
);
} else {
await fs.promises.rm(
videoDir,
{
recursive: true,
force: true,
},
);
console.log(
'[cleanupCast] deleted video',
`age=${formatAge(ageMs)}h`,
videoDir,
);
}
counters.deleted += 1;
}
}
/*
* ============================================================
* CLEANUP GENERALE
* ============================================================
*/
async function cleanupCast({
dryRun = false,
} = {}) {
const startedAt = Date.now();
const now = Date.now();
const counters = {
photoFilesChecked: 0,
videoDirsChecked: 0,
kept: 0,
deleted: 0,
};
console.log(
'[cleanupCast] start',
dryRun ? 'DRY-RUN' : 'LIVE',
);
console.log(
'[cleanupCast] root:',
CAST_ROOT,
);
if (!fs.existsSync(CAST_ROOT)) {
console.log(
'[cleanupCast] cast root does not exist'
);
return counters;
}
const users =
await fs.promises.readdir(
CAST_ROOT,
{
withFileTypes: true,
},
);
for (const userEntry of users) {
if (!userEntry.isDirectory()) {
continue;
}
const userRoot =
path.join(
CAST_ROOT,
userEntry.name,
);
await cleanupPhotos({
photosDir: path.join(
userRoot,
'photos',
),
dryRun,
now,
counters,
});
await cleanupVideos({
videosRoot: path.join(
userRoot,
'videos',
),
dryRun,
now,
counters,
});
}
console.log(
'[cleanupCast] completed',
{
...counters,
dryRun,
elapsedMs:
Date.now() - startedAt,
},
);
return counters;
}
/*
* Esecuzione standalone:
*
* node tools/cleanupCast.js --dry-run
* node tools/cleanupCast.js
*/
if (require.main === module) {
const dryRun =
process.argv.includes('--dry-run');
cleanupCast({
dryRun,
})
.then(() => {
process.exitCode = 0;
})
.catch((error) => {
console.error(
'[cleanupCast] fatal error',
error,
);
process.exitCode = 1;
});
}
module.exports = {
cleanupCast,
MIN_AGE_MS,
CAST_ROOT,
};