Initial release v1.0.0
This commit is contained in:
175
backend/src/services/fileValidator.ts
Normal file
175
backend/src/services/fileValidator.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { prisma } from "../utils/db";
|
||||
import { config } from "../config";
|
||||
import PQueue from "p-queue";
|
||||
|
||||
export interface ValidationResult {
|
||||
tracksChecked: number;
|
||||
tracksRemoved: number;
|
||||
tracksMissing: string[]; // IDs of missing tracks
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export class FileValidatorService {
|
||||
private validationQueue = new PQueue({ concurrency: 50 });
|
||||
|
||||
/**
|
||||
* Validate all tracks in the library and remove missing files
|
||||
*/
|
||||
async validateLibrary(): Promise<ValidationResult> {
|
||||
const startTime = Date.now();
|
||||
const result: ValidationResult = {
|
||||
tracksChecked: 0,
|
||||
tracksRemoved: 0,
|
||||
tracksMissing: [],
|
||||
duration: 0,
|
||||
};
|
||||
|
||||
console.log("[FileValidator] Starting library validation...");
|
||||
|
||||
// Get all tracks from the database
|
||||
const tracks = await prisma.track.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
filePath: true,
|
||||
title: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[FileValidator] Found ${tracks.length} tracks to validate`
|
||||
);
|
||||
|
||||
// Check each track's file existence
|
||||
const missingTrackIds: string[] = [];
|
||||
|
||||
for (const track of tracks) {
|
||||
await this.validationQueue.add(async () => {
|
||||
try {
|
||||
const absolutePath = path.normalize(
|
||||
path.join(config.music.musicPath, track.filePath)
|
||||
);
|
||||
|
||||
// Prevent path traversal attacks
|
||||
if (!absolutePath.startsWith(path.normalize(config.music.musicPath))) {
|
||||
console.warn(
|
||||
`[FileValidator] Path traversal attempt detected: ${track.filePath}`
|
||||
);
|
||||
missingTrackIds.push(track.id);
|
||||
result.tracksChecked++;
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = await this.fileExists(absolutePath);
|
||||
|
||||
if (!exists) {
|
||||
console.log(
|
||||
`[FileValidator] Missing file: ${track.filePath} (${track.title})`
|
||||
);
|
||||
missingTrackIds.push(track.id);
|
||||
}
|
||||
|
||||
result.tracksChecked++;
|
||||
|
||||
// Log progress every 100 tracks
|
||||
if (result.tracksChecked % 100 === 0) {
|
||||
console.log(
|
||||
`[FileValidator] Progress: ${result.tracksChecked}/${tracks.length} tracks checked, ${missingTrackIds.length} missing`
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
`[FileValidator] Error checking ${track.filePath}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await this.validationQueue.onIdle();
|
||||
|
||||
result.tracksMissing = missingTrackIds;
|
||||
|
||||
// Remove missing tracks from database
|
||||
if (missingTrackIds.length > 0) {
|
||||
console.log(
|
||||
`[FileValidator] Removing ${missingTrackIds.length} missing tracks from database...`
|
||||
);
|
||||
|
||||
await prisma.track.deleteMany({
|
||||
where: {
|
||||
id: { in: missingTrackIds },
|
||||
},
|
||||
});
|
||||
|
||||
result.tracksRemoved = missingTrackIds.length;
|
||||
}
|
||||
|
||||
result.duration = Date.now() - startTime;
|
||||
|
||||
console.log(
|
||||
`[FileValidator] Validation complete: ${result.tracksChecked} checked, ${result.tracksRemoved} removed (${result.duration}ms)`
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists (async)
|
||||
*/
|
||||
private async fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single track and remove if missing
|
||||
*/
|
||||
async validateTrack(trackId: string): Promise<boolean> {
|
||||
const track = await prisma.track.findUnique({
|
||||
where: { id: trackId },
|
||||
select: {
|
||||
id: true,
|
||||
filePath: true,
|
||||
title: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!track) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const absolutePath = path.normalize(
|
||||
path.join(config.music.musicPath, track.filePath)
|
||||
);
|
||||
|
||||
// Prevent path traversal attacks
|
||||
if (!absolutePath.startsWith(path.normalize(config.music.musicPath))) {
|
||||
console.warn(
|
||||
`[FileValidator] Path traversal attempt detected: ${track.filePath}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const exists = await this.fileExists(absolutePath);
|
||||
|
||||
if (!exists) {
|
||||
console.log(
|
||||
`[FileValidator] Track file missing, removing from DB: ${track.title}`
|
||||
);
|
||||
await prisma.track.delete({
|
||||
where: { id: trackId },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const fileValidator = new FileValidatorService();
|
||||
Reference in New Issue
Block a user