init
Some checks failed
Build Website / Build NPM Project (push) Failing after 44s
Build Website / Tag Version (push) Has been skipped
Build Website / Build & Push Dockerfile (push) Has been skipped

This commit is contained in:
2025-09-27 00:40:24 -04:00
commit f08a4a0808
20 changed files with 1820 additions and 0 deletions

133
src/services/files.mjs Normal file
View File

@@ -0,0 +1,133 @@
import path from 'path';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream';
import { promisify } from 'util';
import multer from 'multer';
import { promises as fs } from 'fs';
export async function getDirectoryContents(dirPath) {
try {
await fs.access(dirPath);
} catch {
// Create directory if it doesn't exist
await fs.mkdir(dirPath, { recursive: true });
}
const items = await fs.readdir(dirPath, { withFileTypes: true });
const files = [];
console.log(dirPath, items);
for (const item of items) {
const itemPath = path.join(dirPath, item.name);
const stats = await fs.stat(itemPath);
files.push({
name: item.name,
path: itemPath,
isDirectory: item.isDirectory(),
size: item.isDirectory() ? 0 : stats.size,
modified: stats.mtime.toISOString()
});
}
// Sort: directories first, then files, both alphabetically
return files.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
});
}
export function upload(path, zimOnly = false) {
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const uploadPath = req.body.uploadPath || path;
try {
await fs.mkdir(uploadPath, { recursive: true });
cb(null, uploadPath);
} catch (err) {
cb(err);
}
},
filename: (req, file, cb) => {
let finalFilename = file.originalname;
if(req.body.filename && req.body.filename.trim() !== '') {
finalFilename = req.body.filename.trim();
if(!finalFilename.toLowerCase().endsWith('.zim')) {
finalFilename += '.zim';
}
}
cb(null, finalFilename);
}
});
return multer({
storage,
fileFilter: (req, file, cb) => {
if(!zimOnly) cb(null, true);
else if(/\.zim$/i.test(file.originalname)) cb(null, true);
else cb(new Error('Only ZIM files allowed'));
}
});
}
const pipelineAsync = promisify(pipeline);
export async function downloadFile(url, destination) {
if(!url.endsWith('.zim')) throw new Error('Only ZIM files allowed');
if(!destination.endsWith('.zim')) destination += '.zim';
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! ${response.status}`);
const writer = createWriteStream(destination);
await pipelineAsync(response.body, writer);
return destination;
}
export function parseSize(sizeStr) {
if (sizeStr.toLowerCase() === 'infinity') return Infinity;
const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
const match = sizeStr.match(/^(\d+)(B|KB|MB|GB)$/i);
if (!match) return 100 * 1024 * 1024; // default 100MB
return parseInt(match[1]) * units[match[2].toUpperCase()];
}
export async function ensureDirectoryExists(dirPath) {
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
}
export function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
export async function getZimFiles(archiveDir) {
await ensureDirectoryExists(archiveDir);
const items = await fs.readdir(archiveDir, { withFileTypes: true });
const zimFiles = [];
for (const item of items) {
if (item.isFile() && path.extname(item.name).toLowerCase() === '.zim') {
const fullPath = path.join(archiveDir, item.name);
const stats = await fs.stat(fullPath);
zimFiles.push({
name: item.name,
path: fullPath,
size: stats.size,
modified: stats.mtime.toISOString()
});
}
}
return zimFiles.sort((a, b) => new Date(b.modified) - new Date(a.modified));
}
export async function deleteFile(filePath) {
await fs.unlink(filePath);
}