mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
enhance: improve disk space detection and error handling in storage module
- Refactored disk space retrieval logic to support multiple commands based on the operating system. - Added detailed error handling for disk space detection failures, including specific messages for system configuration issues. - Updated API responses to provide clearer error messages to the frontend. - Enhanced the dashboard UI to display loading states and error messages related to disk space retrieval, with retry functionality. - Improved type definitions to accommodate new error handling in the dashboard components.
This commit is contained in:
@@ -1,35 +1,142 @@
|
||||
import { IS_RUNNING_IN_CONTAINER } from "../../utils/container-detection";
|
||||
import { ConfigService } from "../config/service";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { exec } from "child_process";
|
||||
import fs from "node:fs";
|
||||
import { promisify } from "util";
|
||||
import fs from 'node:fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class StorageService {
|
||||
private configService = new ConfigService();
|
||||
private isDockerCached = undefined;
|
||||
|
||||
private _hasDockerEnv() {
|
||||
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,
|
||||
pathToCheck: string
|
||||
): Promise<{ total: number; available: number } | null> {
|
||||
try {
|
||||
fs.statSync('/.dockerenv');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
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; // df -k returns KB, convert to bytes
|
||||
available = this._safeParseInt(avail) * 1024;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Linux
|
||||
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;
|
||||
// Check if command used -B1 (bytes) or default (1K blocks)
|
||||
if (command.includes("-B1")) {
|
||||
total = this._safeParseInt(size);
|
||||
available = this._safeParseInt(avail);
|
||||
} else {
|
||||
// Default df returns 1K blocks
|
||||
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 _hasDockerCGroup() {
|
||||
try {
|
||||
return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
|
||||
const pathsToTry = IS_RUNNING_IN_CONTAINER
|
||||
? ["/app/server/uploads", "/app/server", "/app", "/"]
|
||||
: [".", "./uploads", process.cwd()];
|
||||
|
||||
private _isDocker() {
|
||||
return this.isDockerCached ?? (this._hasDockerEnv() || this._hasDockerCGroup());
|
||||
for (const pathToCheck of pathsToTry) {
|
||||
console.log(`Trying path: ${pathToCheck}`);
|
||||
|
||||
// Ensure the path exists if it's our uploads directory
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if path exists
|
||||
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, pathToCheck);
|
||||
if (result) {
|
||||
console.log(`✅ Successfully got disk space for path: ${pathToCheck}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDiskSpace(
|
||||
@@ -43,49 +150,41 @@ export class StorageService {
|
||||
}> {
|
||||
try {
|
||||
if (isAdmin) {
|
||||
const isDocker = this._isDocker();
|
||||
const pathToCheck = isDocker ? "/app/server/uploads" : ".";
|
||||
console.log(`Running in container: ${IS_RUNNING_IN_CONTAINER}`);
|
||||
|
||||
const command = process.platform === "win32"
|
||||
? "wmic logicaldisk get size,freespace,caption"
|
||||
: process.platform === "darwin"
|
||||
? `df -k ${pathToCheck}`
|
||||
: `df -B1 ${pathToCheck}`;
|
||||
const diskInfo = await this._getDiskSpaceMultiplePaths();
|
||||
|
||||
const { stdout } = await execAsync(command);
|
||||
let total = 0;
|
||||
let available = 0;
|
||||
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");
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const lines = stdout.trim().split("\n").slice(1);
|
||||
for (const line of lines) {
|
||||
const [, size, freespace] = line.trim().split(/\s+/);
|
||||
total += parseInt(size) || 0;
|
||||
available += parseInt(freespace) || 0;
|
||||
}
|
||||
} else if (process.platform === "darwin") {
|
||||
const lines = stdout.trim().split("\n");
|
||||
const [, size, , avail] = lines[1].trim().split(/\s+/);
|
||||
total = parseInt(size) * 1024;
|
||||
available = parseInt(avail) * 1024;
|
||||
} else {
|
||||
const lines = stdout.trim().split("\n");
|
||||
const [, size, , avail] = lines[1].trim().split(/\s+/);
|
||||
total = parseInt(size);
|
||||
available = parseInt(avail);
|
||||
// Only now use fallback, but make it very clear this is an error state
|
||||
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((total / (1024 * 1024 * 1024)).toFixed(2)),
|
||||
diskUsedGB: Number((used / (1024 * 1024 * 1024)).toFixed(2)),
|
||||
diskAvailableGB: Number((available / (1024 * 1024 * 1024)).toFixed(2)),
|
||||
uploadAllowed: true,
|
||||
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 = Number(maxTotalStorage) / (1024 * 1024 * 1024);
|
||||
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
@@ -94,21 +193,26 @@ export class StorageService {
|
||||
|
||||
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
|
||||
|
||||
const usedStorageGB = Number(totalUsedStorage) / (1024 * 1024 * 1024);
|
||||
const availableStorageGB = maxStorageGB - usedStorageGB;
|
||||
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
|
||||
const availableStorageGB = this._ensureNumber(maxStorageGB - usedStorageGB, 0);
|
||||
|
||||
return {
|
||||
diskSizeGB: maxStorageGB,
|
||||
diskUsedGB: usedStorageGB,
|
||||
diskAvailableGB: availableStorageGB,
|
||||
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");
|
||||
console.error("❌ Error getting disk space:", error);
|
||||
|
||||
// Re-throw the error instead of returning fallback values
|
||||
// This way the API will return a proper error and the frontend can handle it
|
||||
throw new Error(
|
||||
`Failed to get disk space information: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user