463 lines
7.5 KiB
JavaScript
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,
|
|
};
|