mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-22 22:02:00 +00:00
- 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.
239 lines
7.8 KiB
TypeScript
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)),
|
|
},
|
|
};
|
|
}
|
|
}
|