mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
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:
@@ -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);
|
||||||
|
@@ -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 },
|
||||||
|
@@ -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 },
|
||||||
|
@@ -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)}`
|
||||||
);
|
);
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user