feat(demo): add DEMO_MODE environment variable and storage limits

- Introduced a new DEMO_MODE environment variable to toggle demo functionality.
- Updated FileController and ReverseShareService to limit user storage to 200MB when DEMO_MODE is enabled.
- Enhanced StorageService to reflect demo storage limits in disk space calculations.
- Added missing authentication providers in the settings form and server start script for better provider management.
This commit is contained in:
Daniel Luiz Alves
2025-07-15 13:45:46 -03:00
parent 383f26e777
commit 794a2782ac
6 changed files with 222 additions and 161 deletions

View File

@@ -14,6 +14,7 @@ const envSchema = z.object({
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"), S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"),
SECURE_SITE: z.union([z.literal("true"), z.literal("false")]).default("false"), SECURE_SITE: z.union([z.literal("true"), z.literal("false")]).default("false"),
DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"), DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"),
DEMO_MODE: z.union([z.literal("true"), z.literal("false")]).default("false"),
}); });
export const env = envSchema.parse(process.env); export const env = envSchema.parse(process.env);

View File

@@ -1,5 +1,6 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { env } from "../../env";
import { prisma } from "../../shared/prisma"; import { prisma } from "../../shared/prisma";
import { ConfigService } from "../config/service"; import { ConfigService } from "../config/service";
import { CheckFileInput, CheckFileSchema, RegisterFileInput, RegisterFileSchema, UpdateFileSchema } from "./dto"; import { CheckFileInput, CheckFileSchema, RegisterFileInput, RegisterFileSchema, UpdateFileSchema } from "./dto";
@@ -55,7 +56,17 @@ export class FileController {
}); });
} }
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser")); // Check if DEMO_MODE is enabled
const isDemoMode = env.DEMO_MODE === "true";
let maxTotalStorage: bigint;
if (isDemoMode) {
// In demo mode, limit all users to 200MB
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
} else {
// Normal behavior - use maxTotalStoragePerUser configuration
maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
}
const userFiles = await prisma.file.findMany({ const userFiles = await prisma.file.findMany({
where: { userId }, where: { userId },
@@ -127,7 +138,17 @@ export class FileController {
}); });
} }
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser")); // Check if DEMO_MODE is enabled
const isDemoMode = env.DEMO_MODE === "true";
let maxTotalStorage: bigint;
if (isDemoMode) {
// In demo mode, limit all users to 200MB
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
} else {
// Normal behavior - use maxTotalStoragePerUser configuration
maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
}
const userFiles = await prisma.file.findMany({ const userFiles = await prisma.file.findMany({
where: { userId }, where: { userId },

View File

@@ -1,5 +1,6 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { env } from "../../env";
import { FileService } from "../file/service"; import { FileService } from "../file/service";
import { import {
CreateReverseShareInput, CreateReverseShareInput,
@@ -513,7 +514,18 @@ export class ReverseShareService {
throw new Error(`File size exceeds the maximum allowed size of ${maxSizeMB}MB`); throw new Error(`File size exceeds the maximum allowed size of ${maxSizeMB}MB`);
} }
const maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser")); // Check if DEMO_MODE is enabled
const isDemoMode = env.DEMO_MODE === "true";
let maxTotalStorage: bigint;
if (isDemoMode) {
// In demo mode, limit all users to 200MB
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
} else {
// Normal behavior - use maxTotalStoragePerUser configuration
maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
}
const userFiles = await prisma.file.findMany({ const userFiles = await prisma.file.findMany({
where: { userId: creatorId }, where: { userId: creatorId },
select: { size: true }, select: { size: true },

View File

@@ -3,14 +3,13 @@ import fs from "node:fs";
import { promisify } from "util"; import { promisify } from "util";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { env } from "../../env";
import { IS_RUNNING_IN_CONTAINER } from "../../utils/container-detection"; import { IS_RUNNING_IN_CONTAINER } from "../../utils/container-detection";
import { ConfigService } from "../config/service"; import { ConfigService } from "../config/service";
const execAsync = promisify(exec); const execAsync = promisify(exec);
const prisma = new PrismaClient(); const prisma = new PrismaClient();
// TODO: REMOVE LOGGING AFTER TESTING
export class StorageService { export class StorageService {
private configService = new ConfigService(); private configService = new ConfigService();
@@ -26,27 +25,23 @@ export class StorageService {
private _parseSize(value: string): number { private _parseSize(value: string): number {
if (!value) return 0; if (!value) return 0;
// Remove any whitespace and convert to lowercase
const cleanValue = value.trim().toLowerCase(); const cleanValue = value.trim().toLowerCase();
// Extract the numeric part
const numericMatch = cleanValue.match(/^(\d+(?:\.\d+)?)/); const numericMatch = cleanValue.match(/^(\d+(?:\.\d+)?)/);
if (!numericMatch) return 0; if (!numericMatch) return 0;
const numericValue = parseFloat(numericMatch[1]); const numericValue = parseFloat(numericMatch[1]);
if (Number.isNaN(numericValue)) return 0; if (Number.isNaN(numericValue)) return 0;
// Determine the unit multiplier
if (cleanValue.includes("t")) { if (cleanValue.includes("t")) {
return Math.round(numericValue * 1024 * 1024 * 1024 * 1024); // TB to bytes return Math.round(numericValue * 1024 * 1024 * 1024 * 1024);
} else if (cleanValue.includes("g")) { } else if (cleanValue.includes("g")) {
return Math.round(numericValue * 1024 * 1024 * 1024); // GB to bytes return Math.round(numericValue * 1024 * 1024 * 1024);
} else if (cleanValue.includes("m")) { } else if (cleanValue.includes("m")) {
return Math.round(numericValue * 1024 * 1024); // MB to bytes return Math.round(numericValue * 1024 * 1024);
} else if (cleanValue.includes("k")) { } else if (cleanValue.includes("k")) {
return Math.round(numericValue * 1024); // KB to bytes return Math.round(numericValue * 1024);
} else { } else {
// Assume bytes if no unit
return Math.round(numericValue); return Math.round(numericValue);
} }
} }
@@ -83,10 +78,8 @@ export class StorageService {
} }
} }
} else { } else {
// Handle different Linux/Unix command formats
const lines = stdout.trim().split("\n"); const lines = stdout.trim().split("\n");
// Handle findmnt command output
if (command.includes("findmnt")) { if (command.includes("findmnt")) {
if (lines.length >= 1) { if (lines.length >= 1) {
const parts = lines[0].trim().split(/\s+/); const parts = lines[0].trim().split(/\s+/);
@@ -96,10 +89,7 @@ export class StorageService {
total = this._parseSize(sizeStr); total = this._parseSize(sizeStr);
} }
} }
} } else if (command.includes("stat -f")) {
// Handle stat -f command output
else if (command.includes("stat -f")) {
// Parse stat -f output (different format)
let blockSize = 0; let blockSize = 0;
let totalBlocks = 0; let totalBlocks = 0;
let freeBlocks = 0; let freeBlocks = 0;
@@ -117,29 +107,19 @@ export class StorageService {
if (blockSize > 0 && totalBlocks > 0) { if (blockSize > 0 && totalBlocks > 0) {
total = totalBlocks * blockSize; total = totalBlocks * blockSize;
available = freeBlocks * blockSize; available = freeBlocks * blockSize;
console.log(
`📊 stat -f parsed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
);
} else { } else {
console.warn(
`❌ stat -f parsing failed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
);
return null; return null;
} }
} } else if (command.includes("--output=")) {
// Handle df --output format
else if (command.includes("--output=")) {
if (lines.length >= 2) { if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/); const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 2) { if (parts.length >= 2) {
const [availStr, sizeStr] = parts; const [availStr, sizeStr] = parts;
available = this._safeParseInt(availStr) * 1024; // df --output gives in KB available = this._safeParseInt(availStr) * 1024;
total = this._safeParseInt(sizeStr) * 1024; total = this._safeParseInt(sizeStr) * 1024;
} }
} }
} } else {
// Handle regular df command output
else {
if (lines.length >= 2) { if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/); const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) { if (parts.length >= 4) {
@@ -148,11 +128,9 @@ export class StorageService {
total = this._safeParseInt(size); total = this._safeParseInt(size);
available = this._safeParseInt(avail); available = this._safeParseInt(avail);
} else if (command.includes("-h")) { } else if (command.includes("-h")) {
// Handle human-readable format (e.g., "1.5G", "500M")
total = this._parseSize(size); total = this._parseSize(size);
available = this._parseSize(avail); available = this._parseSize(avail);
} else { } else {
// Default to KB (standard df output)
total = this._safeParseInt(size) * 1024; total = this._safeParseInt(size) * 1024;
available = this._safeParseInt(avail) * 1024; available = this._safeParseInt(avail) * 1024;
} }
@@ -164,22 +142,16 @@ export class StorageService {
if (total > 0 && available >= 0) { if (total > 0 && available >= 0) {
return { total, available }; return { total, available };
} else { } else {
console.warn(`Invalid values parsed: total=${total}, available=${available} for command: ${command}`);
return null; return null;
} }
} catch (error) { } catch {
console.warn(`Command failed: ${command}`, error);
return null; return null;
} }
} }
/**
* Gets detailed mount information for debugging
*/
private async _getMountInfo(path: string): Promise<{ filesystem: string; mountPoint: string; type: string } | null> { private async _getMountInfo(path: string): Promise<{ filesystem: string; mountPoint: string; type: string } | null> {
try { try {
if (!fs.existsSync("/proc/mounts")) { if (!fs.existsSync("/proc/mounts")) {
console.log("❌ /proc/mounts not found for mount info");
return null; return null;
} }
@@ -189,26 +161,11 @@ export class StorageService {
let bestMatch = null; let bestMatch = null;
let bestMatchLength = 0; let bestMatchLength = 0;
console.log(`🔍 Getting mount info for path: ${path}`);
for (const line of lines) { for (const line of lines) {
const parts = line.split(/\s+/); const parts = line.split(/\s+/);
if (parts.length >= 3) { if (parts.length >= 3) {
const [filesystem, mountPoint, type] = parts; const [filesystem, mountPoint, type] = parts;
// Log interesting filesystems for debugging
if (
filesystem.includes("volume") ||
filesystem.includes("mapper") ||
type === "ext4" ||
type === "btrfs" ||
type === "xfs" ||
mountPoint.includes("volume") ||
mountPoint.includes("app")
) {
console.log(`📋 Mount detail: ${filesystem}${mountPoint} (${type})`);
}
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) { if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
bestMatch = { filesystem, mountPoint, type }; bestMatch = { filesystem, mountPoint, type };
bestMatchLength = mountPoint.length; bestMatchLength = mountPoint.length;
@@ -216,25 +173,15 @@ export class StorageService {
} }
} }
if (bestMatch) {
console.log(`🎯 Selected mount info: ${bestMatch.filesystem}${bestMatch.mountPoint} (${bestMatch.type})`);
}
return bestMatch; return bestMatch;
} catch (error) { } catch {
console.warn(`Could not get mount info for ${path}:`, error);
return null; return null;
} }
} }
/**
* Detects if a path is a bind mount or mount point by checking /proc/mounts
* Returns the actual filesystem path for bind mounts
*/
private async _detectMountPoint(path: string): Promise<string | null> { private async _detectMountPoint(path: string): Promise<string | null> {
try { try {
if (!fs.existsSync("/proc/mounts")) { if (!fs.existsSync("/proc/mounts")) {
console.log("❌ /proc/mounts not found, cannot detect mount points");
return null; return null;
} }
@@ -244,51 +191,32 @@ export class StorageService {
let bestMatch = null; let bestMatch = null;
let bestMatchLength = 0; let bestMatchLength = 0;
console.log(`🔍 Checking ${lines.length} mount points for path: ${path}`);
for (const line of lines) { for (const line of lines) {
const parts = line.split(/\s+/); const parts = line.split(/\s+/);
if (parts.length >= 3) { if (parts.length >= 3) {
const [device, mountPoint, filesystem] = parts; const [device, mountPoint, filesystem] = parts;
// Log useful mount information for debugging
if (mountPoint.includes("volume") || mountPoint.includes("app") || mountPoint === "/") {
console.log(`📍 Found mount: ${device}${mountPoint} (${filesystem})`);
}
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) { if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
bestMatch = mountPoint; bestMatch = mountPoint;
bestMatchLength = mountPoint.length; bestMatchLength = mountPoint.length;
console.log(`✅ Better match found: ${mountPoint} (length: ${mountPoint.length})`);
} }
} }
} }
if (bestMatch && bestMatch !== "/") { if (bestMatch && bestMatch !== "/") {
console.log(`🎯 Selected mount point: ${bestMatch}`);
return bestMatch; return bestMatch;
} }
console.log("❌ No specific mount point found, using root");
return null; return null;
} catch (error) { } catch {
console.warn(`Could not detect mount point for ${path}:`, error);
return null; return null;
} }
} }
/**
* Gets filesystem information for a specific path, with bind mount detection
*/
private async _getFileSystemInfo( private async _getFileSystemInfo(
path: string path: string
): Promise<{ total: number; available: number; mountPoint?: string } | null> { ): Promise<{ total: number; available: number; mountPoint?: string } | null> {
try { try {
const mountInfo = await this._getMountInfo(path); const mountInfo = await this._getMountInfo(path);
if (mountInfo && mountInfo.mountPoint !== "/") {
console.log(`📁 Bind mount detected: ${path}${mountInfo.filesystem} (${mountInfo.type})`);
}
const mountPoint = await this._detectMountPoint(path); const mountPoint = await this._detectMountPoint(path);
const targetPath = mountPoint || path; const targetPath = mountPoint || path;
@@ -298,30 +226,21 @@ export class StorageService {
: process.platform === "darwin" : process.platform === "darwin"
? [`df -k "${targetPath}"`, `df "${targetPath}"`] ? [`df -k "${targetPath}"`, `df "${targetPath}"`]
: [ : [
// Try different df commands for better compatibility
`df -B1 "${targetPath}"`, `df -B1 "${targetPath}"`,
`df -k "${targetPath}"`, `df -k "${targetPath}"`,
`df "${targetPath}"`, `df "${targetPath}"`,
// Additional commands for Synology NAS and other systems
`df -h "${targetPath}"`, `df -h "${targetPath}"`,
`df -T "${targetPath}"`, `df -T "${targetPath}"`,
// Fallback to statfs if available
`stat -f "${targetPath}"`, `stat -f "${targetPath}"`,
// Direct filesystem commands
`findmnt -n -o AVAIL,SIZE "${targetPath}"`, `findmnt -n -o AVAIL,SIZE "${targetPath}"`,
`findmnt -n -o AVAIL,SIZE,TARGET "${targetPath}"`, `findmnt -n -o AVAIL,SIZE,TARGET "${targetPath}"`,
// Alternative df with different formatting
`df -P "${targetPath}"`, `df -P "${targetPath}"`,
`df --output=avail,size "${targetPath}"`, `df --output=avail,size "${targetPath}"`,
]; ];
console.log(`🔍 Trying ${commandsToTry.length} commands for path: ${targetPath}`);
for (const command of commandsToTry) { for (const command of commandsToTry) {
console.log(`🔧 Executing command: ${command}`);
const result = await this._tryDiskSpaceCommand(command); const result = await this._tryDiskSpaceCommand(command);
if (result) { if (result) {
console.log(`✅ Command successful: ${command}`);
return { return {
...result, ...result,
mountPoint: mountPoint || undefined, mountPoint: mountPoint || undefined,
@@ -329,17 +248,12 @@ export class StorageService {
} }
} }
console.warn(`❌ All commands failed for path: ${targetPath}`);
return null; return null;
} catch (error) { } catch {
console.warn(`Error getting filesystem info for ${path}:`, error);
return null; return null;
} }
} }
/**
* Dynamically detect Synology volume paths by reading /proc/mounts
*/
private async _detectSynologyVolumes(): Promise<string[]> { private async _detectSynologyVolumes(): Promise<string[]> {
try { try {
if (!fs.existsSync("/proc/mounts")) { if (!fs.existsSync("/proc/mounts")) {
@@ -355,71 +269,48 @@ export class StorageService {
if (parts.length >= 2) { if (parts.length >= 2) {
const [, mountPoint] = parts; const [, mountPoint] = parts;
// Check if this is a Synology volume mount point
if (mountPoint.match(/^\/volume\d+$/)) { if (mountPoint.match(/^\/volume\d+$/)) {
synologyPaths.push(mountPoint); synologyPaths.push(mountPoint);
console.log(`🔍 Found Synology volume: ${mountPoint}`);
} }
} }
} }
return synologyPaths; return synologyPaths;
} catch (error) { } catch {
console.warn("Could not detect Synology volumes:", error);
return []; return [];
} }
} }
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> { private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
// Base paths that work for all systems
const basePaths = IS_RUNNING_IN_CONTAINER const basePaths = IS_RUNNING_IN_CONTAINER
? ["/app/server/uploads", "/app/server/temp-uploads", "/app/server/temp-chunks", "/app/server", "/app", "/"] ? ["/app/server/uploads", "/app/server/temp-uploads", "/app/server/temp-chunks", "/app/server", "/app", "/"]
: [".", "./uploads", process.cwd()]; : [".", "./uploads", process.cwd()];
// Dynamically detect Synology volume paths
const synologyPaths = await this._detectSynologyVolumes(); const synologyPaths = await this._detectSynologyVolumes();
// Combine base paths with detected Synology paths
const pathsToTry = [...basePaths, ...synologyPaths]; const pathsToTry = [...basePaths, ...synologyPaths];
console.log(`🔍 Attempting disk space detection for ${pathsToTry.length} paths...`);
console.log(`📋 Synology volumes detected: ${synologyPaths.length > 0 ? synologyPaths.join(", ") : "none"}`);
for (const pathToCheck of pathsToTry) { for (const pathToCheck of pathsToTry) {
console.log(`📁 Checking path: ${pathToCheck}`);
if (pathToCheck.includes("uploads") || pathToCheck.includes("temp-")) { if (pathToCheck.includes("uploads") || pathToCheck.includes("temp-")) {
try { try {
if (!fs.existsSync(pathToCheck)) { if (!fs.existsSync(pathToCheck)) {
fs.mkdirSync(pathToCheck, { recursive: true }); fs.mkdirSync(pathToCheck, { recursive: true });
} }
} catch (err) { } catch {
console.warn(`Could not create path ${pathToCheck}:`, err);
continue; continue;
} }
} }
if (!fs.existsSync(pathToCheck)) { if (!fs.existsSync(pathToCheck)) {
console.log(`❌ Path does not exist: ${pathToCheck}`);
continue; continue;
} }
// Use the new filesystem detection method
const result = await this._getFileSystemInfo(pathToCheck); const result = await this._getFileSystemInfo(pathToCheck);
if (result) { if (result) {
if (result.mountPoint) {
console.log(`✅ Storage resolved via bind mount: ${result.mountPoint}`);
}
console.log(
`✅ Disk space detected for path ${pathToCheck}: ${(result.total / (1024 * 1024 * 1024)).toFixed(2)}GB total, ${(result.available / (1024 * 1024 * 1024)).toFixed(2)}GB available`
);
return { total: result.total, available: result.available }; return { total: result.total, available: result.available };
} else {
console.log(`❌ No filesystem info available for path: ${pathToCheck}`);
} }
} }
console.error("❌ All disk space detection attempts failed");
return null; return null;
} }
@@ -433,52 +324,94 @@ export class StorageService {
uploadAllowed: boolean; uploadAllowed: boolean;
}> { }> {
try { try {
const isDemoMode = env.DEMO_MODE === "true";
if (isAdmin) { if (isAdmin) {
const diskInfo = await this._getDiskSpaceMultiplePaths(); if (isDemoMode) {
const demoMaxStorage = 200 * 1024 * 1024;
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
if (!diskInfo) { const userFiles = await prisma.file.findMany({
console.error("❌ Could not determine disk space - system configuration issue"); where: { userId },
throw new Error("Unable to determine actual disk space - system configuration issue"); 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(demoMaxStorageGB - usedStorageGB, 0);
return {
diskSizeGB: Number(demoMaxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
} else {
const diskInfo = await this._getDiskSpaceMultiplePaths();
if (!diskInfo) {
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);
return {
diskSizeGB: Number(diskSizeGB.toFixed(2)),
diskUsedGB: Number(diskUsedGB.toFixed(2)),
diskAvailableGB: Number(diskAvailableGB.toFixed(2)),
uploadAllowed: diskAvailableGB > 0.1,
};
} }
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);
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) { } else if (userId) {
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser")); if (isDemoMode) {
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10); const demoMaxStorage = 200 * 1024 * 1024;
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
const userFiles = await prisma.file.findMany({ const userFiles = await prisma.file.findMany({
where: { userId }, where: { userId },
select: { size: true }, select: { size: true },
}); });
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0)); 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(demoMaxStorageGB - usedStorageGB, 0);
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0); return {
const availableStorageGB = this._ensureNumber(maxStorageGB - usedStorageGB, 0); diskSizeGB: Number(demoMaxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
} else {
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
return { const userFiles = await prisma.file.findMany({
diskSizeGB: Number(maxStorageGB.toFixed(2)), where: { userId },
diskUsedGB: Number(usedStorageGB.toFixed(2)), select: { size: true },
diskAvailableGB: Number(availableStorageGB.toFixed(2)), });
uploadAllowed: availableStorageGB > 0,
}; 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"); throw new Error("User ID is required for non-admin users");
} catch (error) { } catch (error) {
console.error("Error getting disk space:", error); console.error("Error getting disk space:", error);
throw new Error( throw new Error(
`Failed to get disk space information: ${error instanceof Error ? error.message : String(error)}` `Failed to get disk space information: ${error instanceof Error ? error.message : String(error)}`
); );

View File

@@ -104,8 +104,10 @@ export function EditProviderForm({
{ pattern: "linkedin.com", scopes: ["r_liteprofile", "r_emailaddress"] }, { pattern: "linkedin.com", scopes: ["r_liteprofile", "r_emailaddress"] },
{ pattern: "auth0.com", scopes: ["openid", "profile", "email"] }, { pattern: "auth0.com", scopes: ["openid", "profile", "email"] },
{ pattern: "okta.com", scopes: ["openid", "profile", "email"] }, { pattern: "okta.com", scopes: ["openid", "profile", "email"] },
{ pattern: "authentik", scopes: ["openid", "profile", "email"] },
{ pattern: "kinde.com", scopes: ["openid", "profile", "email"] }, { pattern: "kinde.com", scopes: ["openid", "profile", "email"] },
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] }, { pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
{ pattern: "pocketid", scopes: ["openid", "profile", "email"] },
]; ];
for (const { pattern, scopes } of providerPatterns) { for (const { pattern, scopes } of providerPatterns) {

View File

@@ -86,6 +86,19 @@ else
return; return;
} }
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('true');
return;
}
console.log('false'); console.log('false');
} catch (error) { } catch (error) {
console.log('true'); console.log('true');
@@ -117,6 +130,19 @@ else
return; return;
} }
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('true');
return;
}
console.log('false'); console.log('false');
} catch (error) { } catch (error) {
console.log('true'); console.log('true');
@@ -132,6 +158,72 @@ else
if [ "$NEEDS_SEEDING" = "true" ]; then if [ "$NEEDS_SEEDING" = "true" ]; then
echo "🌱 New tables detected or missing data, running seed..." echo "🌱 New tables detected or missing data, running seed..."
# Check which providers are missing for better logging
MISSING_PROVIDERS=$(
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function checkMissingProviders() {
try {
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('Missing providers: ' + missingProviders.join(', '));
} else {
console.log('No missing providers');
}
} catch (error) {
console.log('Error checking providers');
} finally {
await prisma.\$disconnect();
}
}
checkMissingProviders();
" 2>/dev/null || echo "Error checking providers"
else
node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function checkMissingProviders() {
try {
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('Missing providers: ' + missingProviders.join(', '));
} else {
console.log('No missing providers');
}
} catch (error) {
console.log('Error checking providers');
} finally {
await prisma.\$disconnect();
}
}
checkMissingProviders();
" 2>/dev/null || echo "Error checking providers"
fi
)
if [ "$MISSING_PROVIDERS" != "No missing providers" ] && [ "$MISSING_PROVIDERS" != "Error checking providers" ]; then
echo "🔍 $MISSING_PROVIDERS"
fi
if [ "$(id -u)" = "0" ]; then if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
else else