Files
Palmr/apps/server/src/modules/storage/service.ts
Daniel Luiz Alves 75d6049b87 feat: enhance pre-push validation and update ESLint configurations
- Updated the Husky pre-push hook to validate all applications (web, docs, and server) before pushing changes, improving code quality checks.
- Modified ESLint configurations for the docs app to include additional ignored directories, ensuring cleaner linting results.
- Refactored the HomePage component in the docs app to improve structure and readability, while reintroducing the Highlight component for better content presentation.
- Added a .prettierignore file in the server app to exclude specific directories from formatting, enhancing development workflow.
- Updated various import statements across multiple files for consistency and clarity.
2025-07-02 14:53:23 -03:00

239 lines
7.8 KiB
TypeScript

import { exec } from "child_process";
import fs from "node:fs";
import { promisify } from "util";
import { PrismaClient } from "@prisma/client";
import { IS_RUNNING_IN_CONTAINER } from "../../utils/container-detection";
import { ConfigService } from "../config/service";
const execAsync = promisify(exec);
const prisma = new PrismaClient();
export class StorageService {
private configService = new ConfigService();
private _ensureNumber(value: number, fallback: number = 0): number {
if (isNaN(value) || !isFinite(value)) {
return fallback;
}
return value;
}
private _safeParseInt(value: string): number {
const parsed = parseInt(value);
return this._ensureNumber(parsed, 0);
}
private async _tryDiskSpaceCommand(command: string): Promise<{ total: number; available: number } | null> {
try {
console.log(`Trying disk space command: ${command}`);
const { stdout, stderr } = await execAsync(command);
if (stderr) {
console.warn(`Command stderr: ${stderr}`);
}
console.log(`Command stdout: ${stdout}`);
let total = 0;
let available = 0;
if (process.platform === "win32") {
const lines = stdout.trim().split("\n").slice(1);
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 3) {
const [, size, freespace] = parts;
total += this._safeParseInt(size);
available += this._safeParseInt(freespace);
}
}
} else if (process.platform === "darwin") {
const lines = stdout.trim().split("\n");
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) {
const [, size, , avail] = parts;
total = this._safeParseInt(size) * 1024;
available = this._safeParseInt(avail) * 1024;
}
}
} else {
const lines = stdout.trim().split("\n");
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) {
const [, size, , avail] = parts;
if (command.includes("-B1")) {
total = this._safeParseInt(size);
available = this._safeParseInt(avail);
} else {
total = this._safeParseInt(size) * 1024;
available = this._safeParseInt(avail) * 1024;
}
}
}
}
if (total > 0 && available >= 0) {
console.log(`Successfully parsed disk space: ${total} bytes total, ${available} bytes available`);
return { total, available };
} else {
console.warn(`Invalid values parsed: total=${total}, available=${available}`);
return null;
}
} catch (error) {
console.warn(`Command failed: ${command}`, error);
return null;
}
}
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
const pathsToTry = IS_RUNNING_IN_CONTAINER
? ["/app/server/uploads", "/app/server", "/app", "/"]
: [".", "./uploads", process.cwd()];
for (const pathToCheck of pathsToTry) {
console.log(`Trying path: ${pathToCheck}`);
if (pathToCheck.includes("uploads")) {
try {
if (!fs.existsSync(pathToCheck)) {
fs.mkdirSync(pathToCheck, { recursive: true });
console.log(`Created directory: ${pathToCheck}`);
}
} catch (err) {
console.warn(`Could not create path ${pathToCheck}:`, err);
continue;
}
}
if (!fs.existsSync(pathToCheck)) {
console.warn(`Path does not exist: ${pathToCheck}`);
continue;
}
const commandsToTry =
process.platform === "win32"
? ["wmic logicaldisk get size,freespace,caption"]
: process.platform === "darwin"
? [`df -k "${pathToCheck}"`, `df "${pathToCheck}"`]
: [`df -B1 "${pathToCheck}"`, `df -k "${pathToCheck}"`, `df "${pathToCheck}"`];
for (const command of commandsToTry) {
const result = await this._tryDiskSpaceCommand(command);
if (result) {
console.log(`✅ Successfully got disk space for path: ${pathToCheck}`);
return result;
}
}
}
return null;
}
async getDiskSpace(
userId?: string,
isAdmin?: boolean
): Promise<{
diskSizeGB: number;
diskUsedGB: number;
diskAvailableGB: number;
uploadAllowed: boolean;
}> {
try {
if (isAdmin) {
console.log(`Running in container: ${IS_RUNNING_IN_CONTAINER}`);
const diskInfo = await this._getDiskSpaceMultiplePaths();
if (!diskInfo) {
console.error("❌ CRITICAL: Could not determine disk space using any method!");
console.error("This indicates a serious system issue. Please check:");
console.error("1. File system permissions");
console.error("2. Available disk utilities (df, wmic)");
console.error("3. Container/system configuration");
throw new Error("Unable to determine actual disk space - system configuration issue");
}
const { total, available } = diskInfo;
const used = total - available;
const diskSizeGB = this._ensureNumber(total / (1024 * 1024 * 1024), 0);
const diskUsedGB = this._ensureNumber(used / (1024 * 1024 * 1024), 0);
const diskAvailableGB = this._ensureNumber(available / (1024 * 1024 * 1024), 0);
console.log(
`✅ Real disk space: ${diskSizeGB.toFixed(2)}GB total, ${diskUsedGB.toFixed(2)}GB used, ${diskAvailableGB.toFixed(2)}GB available`
);
return {
diskSizeGB: Number(diskSizeGB.toFixed(2)),
diskUsedGB: Number(diskUsedGB.toFixed(2)),
diskAvailableGB: Number(diskAvailableGB.toFixed(2)),
uploadAllowed: diskAvailableGB > 0.1, // At least 100MB free
};
} else if (userId) {
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
const availableStorageGB = this._ensureNumber(maxStorageGB - usedStorageGB, 0);
return {
diskSizeGB: Number(maxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
}
throw new Error("User ID is required for non-admin users");
} catch (error) {
console.error("❌ Error getting disk space:", error);
throw new Error(
`Failed to get disk space information: ${error instanceof Error ? error.message : String(error)}`
);
}
}
async checkUploadAllowed(
fileSize: number,
userId?: string
): Promise<{
diskSizeGB: number;
diskUsedGB: number;
diskAvailableGB: number;
uploadAllowed: boolean;
fileSizeInfo: {
bytes: number;
kb: number;
mb: number;
gb: number;
};
}> {
const diskSpace = await this.getDiskSpace(userId);
const fileSizeGB = fileSize / (1024 * 1024 * 1024);
return {
...diskSpace,
uploadAllowed: diskSpace.diskAvailableGB > fileSizeGB,
fileSizeInfo: {
bytes: fileSize,
kb: Number((fileSize / 1024).toFixed(2)),
mb: Number((fileSize / (1024 * 1024)).toFixed(2)),
gb: Number((fileSize / (1024 * 1024 * 1024)).toFixed(2)),
},
};
}
}