mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-22 22:02:00 +00:00
chore: add DISABLE_FILESYSTEM_ENCRYPTION option
- Enhanced comments in docker-compose files to clarify the purpose of environment variables, including optional settings for UID, GID, and filesystem encryption. - Introduced DISABLE_FILESYSTEM_ENCRYPTION variable to allow users to disable file encryption, making the ENCRYPTION_KEY optional. - Updated documentation in quick-start guide to reflect changes in environment variable usage and security warnings.
This commit is contained in:
@@ -133,6 +133,7 @@ set -e
|
||||
echo "Starting Palmr Application..."
|
||||
echo "Storage Mode: \${ENABLE_S3:-false}"
|
||||
echo "Secure Site: \${SECURE_SITE:-false}"
|
||||
echo "Encryption: \${DISABLE_FILESYSTEM_ENCRYPTION:-false}"
|
||||
echo "Database: SQLite"
|
||||
|
||||
# Set global environment variables
|
||||
|
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
> palmr-docs@3.1-beta lint /Users/daniel/clones/Palmr/apps/docs
|
||||
> eslint "src/**/*.+(ts|tsx)"
|
||||
|
@@ -56,6 +56,7 @@ services:
|
||||
environment:
|
||||
- ENABLE_S3=false
|
||||
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
|
||||
# - DISABLE_FILESYSTEM_ENCRYPTION=false # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional)
|
||||
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
ports:
|
||||
- "5487:5487" # Web interface
|
||||
@@ -102,9 +103,10 @@ services:
|
||||
environment:
|
||||
- ENABLE_S3=false
|
||||
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
|
||||
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
- PALMR_UID=1000 # UID for the container processes (default is 1001)
|
||||
- PALMR_GID=1000 # GID for the container processes (default is 1001)
|
||||
# - DISABLE_FILESYSTEM_ENCRYPTION=false # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional)
|
||||
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
ports:
|
||||
- "5487:5487" # Web port
|
||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
||||
@@ -128,13 +130,16 @@ docker-compose up -d
|
||||
|
||||
Configure Palmr. behavior through environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ------- | ------------------------------------------------------- |
|
||||
| `ENABLE_S3` | `false` | Enable S3-compatible storage |
|
||||
| `ENCRYPTION_KEY` | - | **Required**: Minimum 32 characters for file encryption |
|
||||
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy setups |
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
||||
| `ENABLE_S3` | `false` | Enable S3-compatible storage |
|
||||
| `ENCRYPTION_KEY` | - | **Required** (unless encryption disabled): Minimum 32 characters for file encryption |
|
||||
| `DISABLE_FILESYSTEM_ENCRYPTION` | `false` | Disable file encryption for direct filesystem access |
|
||||
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy setups |
|
||||
|
||||
> **⚠️ Security Warning**: Always change the `ENCRYPTION_KEY` in production. This key encrypts your files - losing it makes files permanently inaccessible.
|
||||
> **⚠️ Security Warning**: Always change the `ENCRYPTION_KEY` in production when encryption is enabled. This key encrypts your files - losing it makes files permanently inaccessible.
|
||||
|
||||
> **🔓 File Encryption Control**: The `DISABLE_FILESYSTEM_ENCRYPTION` variable allows you to store files without encryption for direct filesystem access. When set to `true`, the `ENCRYPTION_KEY` becomes optional. **Important**: Once set, this configuration is permanent for your deployment. Switching between encrypted and unencrypted modes will break file access for existing uploads. Choose your strategy before uploading files.
|
||||
|
||||
> **🔗 Reverse Proxy**: If deploying behind a reverse proxy (Traefik, Nginx, etc.), set `SECURE_SITE=true` and review our [Reverse Proxy Configuration](/docs/3.1-beta/reverse-proxy-configuration) guide for proper setup.
|
||||
|
||||
@@ -144,6 +149,8 @@ Need a strong key for `ENCRYPTION_KEY`? Use our built-in generator to create cry
|
||||
|
||||
<KeyGenerator />
|
||||
|
||||
> **💡 Pro Tip**: If you're using `DISABLE_FILESYSTEM_ENCRYPTION=true`, you can skip the `ENCRYPTION_KEY` entirely for a simpler setup. However, remember that files will be stored unencrypted on your filesystem.
|
||||
|
||||
---
|
||||
|
||||
## Accessing Palmr.
|
||||
@@ -177,6 +184,8 @@ docker run -d \
|
||||
--name palmr \
|
||||
-e ENABLE_S3=false \
|
||||
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
|
||||
# -e DISABLE_FILESYSTEM_ENCRYPTION=true # Uncomment to disable file encryption (ENCRYPTION_KEY becomes optional)
|
||||
# -e SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
-p 5487:5487 \
|
||||
-p 3333:3333 \
|
||||
-v palmr_data:/app/server \
|
||||
@@ -184,6 +193,8 @@ docker run -d \
|
||||
kyantech/palmr:latest
|
||||
```
|
||||
|
||||
> **Permission Configuration**: If you encounter permission issues with bind mounts (common on NAS systems), see our [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) guide for automatic permission handling.
|
||||
|
||||
**Bind Mount:**
|
||||
|
||||
```bash
|
||||
@@ -191,6 +202,10 @@ docker run -d \
|
||||
--name palmr \
|
||||
-e ENABLE_S3=false \
|
||||
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
|
||||
-e PALMR_UID=1000 # UID for the container processes (default is 1001)
|
||||
-e PALMR_GID=1000 # GID for the container processes (default is 1001)
|
||||
# -e DISABLE_FILESYSTEM_ENCRYPTION=true # Uncomment to disable file encryption (ENCRYPTION_KEY becomes optional)
|
||||
# -e SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
-p 5487:5487 \
|
||||
-p 3333:3333 \
|
||||
-v $(pwd)/data:/app/server \
|
||||
|
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
> palmr-api@3.1-beta lint /Users/daniel/clones/Palmr/apps/server
|
||||
> eslint "src/**/*.+(ts|tsx)"
|
||||
|
@@ -24,7 +24,7 @@ export async function buildApp() {
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
level: "info",
|
||||
level: "warn",
|
||||
},
|
||||
bodyLimit: 1024 * 1024 * 1024 * 1024 * 1024,
|
||||
connectionTimeout: 0,
|
||||
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
const envSchema = z.object({
|
||||
ENABLE_S3: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
||||
ENCRYPTION_KEY: z.string().optional().default("palmr-default-encryption-key-2025"),
|
||||
DISABLE_FILESYSTEM_ENCRYPTION: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
||||
S3_ENDPOINT: z.string().optional(),
|
||||
S3_PORT: z.string().optional(),
|
||||
S3_USE_SSL: z.string().optional(),
|
||||
|
@@ -92,7 +92,6 @@ export class ChunkManager {
|
||||
console.log(`Chunk ${chunkIndex} already uploaded, treating as success`);
|
||||
|
||||
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
|
||||
// Check if already finalizing to prevent race condition
|
||||
if (this.finalizingUploads.has(fileId)) {
|
||||
console.log(`Upload ${fileId} is already being finalized, waiting...`);
|
||||
return { isComplete: false };
|
||||
@@ -128,7 +127,6 @@ export class ChunkManager {
|
||||
);
|
||||
|
||||
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
|
||||
// Check if already finalizing to prevent race condition
|
||||
if (this.finalizingUploads.has(fileId)) {
|
||||
console.log(`Upload ${fileId} is already being finalized, waiting...`);
|
||||
return { isComplete: false };
|
||||
@@ -199,7 +197,7 @@ export class ChunkManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize upload by moving temp file to final location and encrypting
|
||||
* Finalize upload by moving temp file to final location and encrypting (if enabled)
|
||||
*/
|
||||
private async finalizeUpload(
|
||||
chunkInfo: ChunkInfo,
|
||||
@@ -224,7 +222,7 @@ export class ChunkManager {
|
||||
const filePath = provider.getFilePath(finalObjectName);
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
console.log(`Starting encryption and finalization: ${finalObjectName}`);
|
||||
console.log(`Starting finalization: ${finalObjectName}`);
|
||||
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
|
||||
@@ -236,7 +234,6 @@ export class ChunkManager {
|
||||
});
|
||||
const encryptStream = provider.createEncryptStream();
|
||||
|
||||
// Wait for encryption to complete BEFORE cleaning up temp file
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -245,18 +242,17 @@ export class ChunkManager {
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`File encrypted and saved to: ${filePath} in ${duration}ms`);
|
||||
console.log(`File processed and saved to: ${filePath} in ${duration}ms`);
|
||||
resolve();
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error("Error during encryption:", error);
|
||||
console.error("Error during processing:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`File successfully uploaded and encrypted: ${finalObjectName}`);
|
||||
console.log(`File successfully uploaded and processed: ${finalObjectName}`);
|
||||
|
||||
// Clean up temp file AFTER encryption is complete
|
||||
await this.cleanupTempFile(chunkInfo.tempPath);
|
||||
|
||||
this.activeUploads.delete(chunkInfo.fileId);
|
||||
|
@@ -9,6 +9,8 @@ import { ConfigService } from "../config/service";
|
||||
const execAsync = promisify(exec);
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// TODO: REMOVE LOGGING AFTER TESTING
|
||||
|
||||
export class StorageService {
|
||||
private configService = new ConfigService();
|
||||
|
||||
@@ -21,6 +23,34 @@ export class StorageService {
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
private _parseSize(value: string): number {
|
||||
if (!value) return 0;
|
||||
|
||||
// Remove any whitespace and convert to lowercase
|
||||
const cleanValue = value.trim().toLowerCase();
|
||||
|
||||
// Extract the numeric part
|
||||
const numericMatch = cleanValue.match(/^(\d+(?:\.\d+)?)/);
|
||||
if (!numericMatch) return 0;
|
||||
|
||||
const numericValue = parseFloat(numericMatch[1]);
|
||||
if (Number.isNaN(numericValue)) return 0;
|
||||
|
||||
// Determine the unit multiplier
|
||||
if (cleanValue.includes("t")) {
|
||||
return Math.round(numericValue * 1024 * 1024 * 1024 * 1024); // TB to bytes
|
||||
} else if (cleanValue.includes("g")) {
|
||||
return Math.round(numericValue * 1024 * 1024 * 1024); // GB to bytes
|
||||
} else if (cleanValue.includes("m")) {
|
||||
return Math.round(numericValue * 1024 * 1024); // MB to bytes
|
||||
} else if (cleanValue.includes("k")) {
|
||||
return Math.round(numericValue * 1024); // KB to bytes
|
||||
} else {
|
||||
// Assume bytes if no unit
|
||||
return Math.round(numericValue);
|
||||
}
|
||||
}
|
||||
|
||||
private async _tryDiskSpaceCommand(command: string): Promise<{ total: number; available: number } | null> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
@@ -53,17 +83,79 @@ export class StorageService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle different Linux/Unix command formats
|
||||
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;
|
||||
|
||||
// Handle findmnt command output
|
||||
if (command.includes("findmnt")) {
|
||||
if (lines.length >= 1) {
|
||||
const parts = lines[0].trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const [availStr, sizeStr] = parts;
|
||||
available = this._parseSize(availStr);
|
||||
total = this._parseSize(sizeStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle stat -f command output
|
||||
else if (command.includes("stat -f")) {
|
||||
// Parse stat -f output (different format)
|
||||
let blockSize = 0;
|
||||
let totalBlocks = 0;
|
||||
let freeBlocks = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes("Block size:")) {
|
||||
blockSize = this._safeParseInt(line.split(":")[1].trim());
|
||||
} else if (line.includes("Total blocks:")) {
|
||||
totalBlocks = this._safeParseInt(line.split(":")[1].trim());
|
||||
} else if (line.includes("Free blocks:")) {
|
||||
freeBlocks = this._safeParseInt(line.split(":")[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (blockSize > 0 && totalBlocks > 0) {
|
||||
total = totalBlocks * blockSize;
|
||||
available = freeBlocks * blockSize;
|
||||
console.log(
|
||||
`📊 stat -f parsed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`❌ stat -f parsing failed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Handle df --output format
|
||||
else if (command.includes("--output=")) {
|
||||
if (lines.length >= 2) {
|
||||
const parts = lines[1].trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const [availStr, sizeStr] = parts;
|
||||
available = this._safeParseInt(availStr) * 1024; // df --output gives in KB
|
||||
total = this._safeParseInt(sizeStr) * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle regular df command output
|
||||
else {
|
||||
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 if (command.includes("-h")) {
|
||||
// Handle human-readable format (e.g., "1.5G", "500M")
|
||||
total = this._parseSize(size);
|
||||
available = this._parseSize(avail);
|
||||
} else {
|
||||
// Default to KB (standard df output)
|
||||
total = this._safeParseInt(size) * 1024;
|
||||
available = this._safeParseInt(avail) * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +164,7 @@ export class StorageService {
|
||||
if (total > 0 && available >= 0) {
|
||||
return { total, available };
|
||||
} else {
|
||||
console.warn(`Invalid values parsed: total=${total}, available=${available}`);
|
||||
console.warn(`Invalid values parsed: total=${total}, available=${available} for command: ${command}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -87,6 +179,7 @@ export class StorageService {
|
||||
private async _getMountInfo(path: string): Promise<{ filesystem: string; mountPoint: string; type: string } | null> {
|
||||
try {
|
||||
if (!fs.existsSync("/proc/mounts")) {
|
||||
console.log("❌ /proc/mounts not found for mount info");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -96,11 +189,26 @@ export class StorageService {
|
||||
let bestMatch = null;
|
||||
let bestMatchLength = 0;
|
||||
|
||||
console.log(`🔍 Getting mount info for path: ${path}`);
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 3) {
|
||||
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) {
|
||||
bestMatch = { filesystem, mountPoint, type };
|
||||
bestMatchLength = mountPoint.length;
|
||||
@@ -108,6 +216,10 @@ export class StorageService {
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch) {
|
||||
console.log(`🎯 Selected mount info: ${bestMatch.filesystem} → ${bestMatch.mountPoint} (${bestMatch.type})`);
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
} catch (error) {
|
||||
console.warn(`Could not get mount info for ${path}:`, error);
|
||||
@@ -122,6 +234,7 @@ export class StorageService {
|
||||
private async _detectMountPoint(path: string): Promise<string | null> {
|
||||
try {
|
||||
if (!fs.existsSync("/proc/mounts")) {
|
||||
console.log("❌ /proc/mounts not found, cannot detect mount points");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -131,22 +244,32 @@ export class StorageService {
|
||||
let bestMatch = null;
|
||||
let bestMatchLength = 0;
|
||||
|
||||
console.log(`🔍 Checking ${lines.length} mount points for path: ${path}`);
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const [, mountPoint] = parts;
|
||||
if (parts.length >= 3) {
|
||||
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) {
|
||||
bestMatch = mountPoint;
|
||||
bestMatchLength = mountPoint.length;
|
||||
console.log(`✅ Better match found: ${mountPoint} (length: ${mountPoint.length})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch && bestMatch !== "/") {
|
||||
console.log(`🎯 Selected mount point: ${bestMatch}`);
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
console.log("❌ No specific mount point found, using root");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn(`Could not detect mount point for ${path}:`, error);
|
||||
@@ -174,11 +297,31 @@ export class StorageService {
|
||||
? ["wmic logicaldisk get size,freespace,caption"]
|
||||
: process.platform === "darwin"
|
||||
? [`df -k "${targetPath}"`, `df "${targetPath}"`]
|
||||
: [`df -B1 "${targetPath}"`, `df -k "${targetPath}"`, `df "${targetPath}"`];
|
||||
: [
|
||||
// Try different df commands for better compatibility
|
||||
`df -B1 "${targetPath}"`,
|
||||
`df -k "${targetPath}"`,
|
||||
`df "${targetPath}"`,
|
||||
// Additional commands for Synology NAS and other systems
|
||||
`df -h "${targetPath}"`,
|
||||
`df -T "${targetPath}"`,
|
||||
// Fallback to statfs if available
|
||||
`stat -f "${targetPath}"`,
|
||||
// Direct filesystem commands
|
||||
`findmnt -n -o AVAIL,SIZE "${targetPath}"`,
|
||||
`findmnt -n -o AVAIL,SIZE,TARGET "${targetPath}"`,
|
||||
// Alternative df with different formatting
|
||||
`df -P "${targetPath}"`,
|
||||
`df --output=avail,size "${targetPath}"`,
|
||||
];
|
||||
|
||||
console.log(`🔍 Trying ${commandsToTry.length} commands for path: ${targetPath}`);
|
||||
|
||||
for (const command of commandsToTry) {
|
||||
console.log(`🔧 Executing command: ${command}`);
|
||||
const result = await this._tryDiskSpaceCommand(command);
|
||||
if (result) {
|
||||
console.log(`✅ Command successful: ${command}`);
|
||||
return {
|
||||
...result,
|
||||
mountPoint: mountPoint || undefined,
|
||||
@@ -186,6 +329,7 @@ export class StorageService {
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`❌ All commands failed for path: ${targetPath}`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn(`Error getting filesystem info for ${path}:`, error);
|
||||
@@ -193,13 +337,58 @@ export class StorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically detect Synology volume paths by reading /proc/mounts
|
||||
*/
|
||||
private async _detectSynologyVolumes(): Promise<string[]> {
|
||||
try {
|
||||
if (!fs.existsSync("/proc/mounts")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mountsContent = await fs.promises.readFile("/proc/mounts", "utf8");
|
||||
const lines = mountsContent.split("\n").filter((line) => line.trim());
|
||||
const synologyPaths: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const [, mountPoint] = parts;
|
||||
|
||||
// Check if this is a Synology volume mount point
|
||||
if (mountPoint.match(/^\/volume\d+$/)) {
|
||||
synologyPaths.push(mountPoint);
|
||||
console.log(`🔍 Found Synology volume: ${mountPoint}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return synologyPaths;
|
||||
} catch (error) {
|
||||
console.warn("Could not detect Synology volumes:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
|
||||
const pathsToTry = IS_RUNNING_IN_CONTAINER
|
||||
? ["/app/server/uploads", "/app/server", "/app", "/"]
|
||||
// Base paths that work for all systems
|
||||
const basePaths = IS_RUNNING_IN_CONTAINER
|
||||
? ["/app/server/uploads", "/app/server/temp-uploads", "/app/server/temp-chunks", "/app/server", "/app", "/"]
|
||||
: [".", "./uploads", process.cwd()];
|
||||
|
||||
// Dynamically detect Synology volume paths
|
||||
const synologyPaths = await this._detectSynologyVolumes();
|
||||
|
||||
// Combine base paths with detected Synology paths
|
||||
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) {
|
||||
if (pathToCheck.includes("uploads")) {
|
||||
console.log(`📁 Checking path: ${pathToCheck}`);
|
||||
|
||||
if (pathToCheck.includes("uploads") || pathToCheck.includes("temp-")) {
|
||||
try {
|
||||
if (!fs.existsSync(pathToCheck)) {
|
||||
fs.mkdirSync(pathToCheck, { recursive: true });
|
||||
@@ -211,6 +400,7 @@ export class StorageService {
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pathToCheck)) {
|
||||
console.log(`❌ Path does not exist: ${pathToCheck}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -220,10 +410,16 @@ export class StorageService {
|
||||
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 };
|
||||
} else {
|
||||
console.log(`❌ No filesystem info available for path: ${pathToCheck}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.error("❌ All disk space detection attempts failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@@ -8,12 +8,12 @@ import { pipeline } from "stream/promises";
|
||||
import { directoriesConfig, getTempFilePath } from "../config/directories.config";
|
||||
import { env } from "../env";
|
||||
import { StorageProvider } from "../types/storage";
|
||||
import { IS_RUNNING_IN_CONTAINER } from "../utils/container-detection";
|
||||
|
||||
export class FilesystemStorageProvider implements StorageProvider {
|
||||
private static instance: FilesystemStorageProvider;
|
||||
private uploadsDir: string;
|
||||
private encryptionKey = env.ENCRYPTION_KEY;
|
||||
private isEncryptionDisabled = env.DISABLE_FILESYSTEM_ENCRYPTION === "true";
|
||||
private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
|
||||
private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
|
||||
|
||||
@@ -66,6 +66,15 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
public createEncryptStream(): Transform {
|
||||
if (this.isEncryptionDisabled) {
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const key = this.createEncryptionKey();
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
@@ -101,6 +110,15 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
public createDecryptStream(): Transform {
|
||||
if (this.isEncryptionDisabled) {
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const key = this.createEncryptionKey();
|
||||
let iv: Buffer | null = null;
|
||||
let decipher: crypto.Decipher | null = null;
|
||||
@@ -213,32 +231,26 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private encryptFileBuffer(buffer: Buffer): Buffer {
|
||||
const key = this.createEncryptionKey();
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([iv, cipher.update(buffer), cipher.final()]);
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
async downloadFile(objectName: string): Promise<Buffer> {
|
||||
const filePath = this.getFilePath(objectName);
|
||||
const encryptedBuffer = await fs.readFile(filePath);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
if (encryptedBuffer.length > 16) {
|
||||
if (this.isEncryptionDisabled) {
|
||||
return fileBuffer;
|
||||
}
|
||||
|
||||
if (fileBuffer.length > 16) {
|
||||
try {
|
||||
return this.decryptFileBuffer(encryptedBuffer);
|
||||
return this.decryptFileBuffer(fileBuffer);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.warn("Failed to decrypt with new method, trying legacy format", error.message);
|
||||
}
|
||||
return this.decryptFileLegacy(encryptedBuffer);
|
||||
return this.decryptFileLegacy(fileBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
return this.decryptFileLegacy(encryptedBuffer);
|
||||
return this.decryptFileLegacy(fileBuffer);
|
||||
}
|
||||
|
||||
private decryptFileBuffer(encryptedBuffer: Buffer): Buffer {
|
||||
|
@@ -101,7 +101,9 @@ async function startServer() {
|
||||
}
|
||||
|
||||
console.log(`🌴 Palmr server running on port 3333 🌴`);
|
||||
console.log(`📦 Storage mode: ${env.ENABLE_S3 === "true" ? "S3" : "Local Filesystem (Encrypted)"}`);
|
||||
console.log(
|
||||
`📦 Storage mode: ${env.ENABLE_S3 === "true" ? "S3" : `Local Filesystem ${env.DISABLE_FILESYSTEM_ENCRYPTION === "true" ? "(Unencrypted)" : "(Encrypted)"}`}`
|
||||
);
|
||||
console.log(`🔐 Auth Providers: ${authProviders}`);
|
||||
|
||||
console.log("\n📚 API Documentation:");
|
||||
|
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
> palmr-web@3.1-beta lint /Users/daniel/clones/Palmr/apps/web
|
||||
> eslint "src/**/*.+(ts|tsx)"
|
||||
|
@@ -4,10 +4,11 @@ services:
|
||||
container_name: palmr
|
||||
environment:
|
||||
- ENABLE_S3=false
|
||||
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
|
||||
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
|
||||
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
|
||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY (REQUIRED if DISABLE_FILESYSTEM_ENCRYPTION is false)
|
||||
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||
# - DISABLE_FILESYSTEM_ENCRYPTION=true # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional) | (OPTIONAL - default is false)
|
||||
ports:
|
||||
- "5487:5487" # Web port
|
||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
||||
|
@@ -12,9 +12,9 @@ services:
|
||||
- S3_REGION=${S3_REGION:-us-east-1} # S3 region (us-east-1 is the default region) but it depends on your s3 server region
|
||||
- S3_BUCKET_NAME=${S3_BUCKET_NAME:-palmr-files} # Bucket name for the S3 storage (here we are using palmr-files as the bucket name to understand that this is the bucket for palmr)
|
||||
- S3_FORCE_PATH_STYLE=true # For MinIO compatibility we have to set this to true
|
||||
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
|
||||
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
|
||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||
ports:
|
||||
- "5487:5487" # Web port
|
||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
||||
|
@@ -12,9 +12,9 @@ services:
|
||||
- S3_REGION=${S3_REGION:-us-east-1} # S3 region (us-east-1 is the default region) but it depends on your s3 server region
|
||||
- S3_BUCKET_NAME=${S3_BUCKET_NAME:-palmr-files} # Bucket name for the S3 storage (here we are using palmr-files as the bucket name to understand that this is the bucket for palmr)
|
||||
- S3_FORCE_PATH_STYLE=false # For S3 compatibility we have to set this to false
|
||||
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
|
||||
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
|
||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||
ports:
|
||||
- "5487:5487" # Web port
|
||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
||||
|
23
docker-compose-synology-test.yaml
Normal file
23
docker-compose-synology-test.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
palmr:
|
||||
image: kyantech/palmr:v3.1.1-rc.1
|
||||
container_name: palmr
|
||||
environment:
|
||||
- ENABLE_S3=false
|
||||
- ENCRYPTION_KEY=palmr-test-encryption-key-2025
|
||||
- PALMR_UID=1000 # UID for Synology NAS compatibility (default is 1001) | See our UID/GID Documentation for more information
|
||||
- PALMR_GID=1000 # GID for Synology NAS compatibility (default is 1001) | See our UID/GID Documentation for more information
|
||||
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||
# - DISABLE_FILESYSTEM_ENCRYPTION=true # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional) | (OPTIONAL - default is false)
|
||||
# Add any other environment variables as needed
|
||||
ports:
|
||||
- "5487:5487"
|
||||
- "3333:3333"
|
||||
volumes:
|
||||
- /volume1/docker/palmr/uploads:/app/server/uploads
|
||||
- /volume1/docker/palmr/temp-uploads:/app/server/temp-uploads
|
||||
- /volume1/docker/palmr/temp-chunks:/app/server/temp-chunks
|
||||
- /volume1/docker/palmr/prisma:/app/server/prisma
|
||||
restart: unless-stopped
|
@@ -4,13 +4,14 @@ services:
|
||||
container_name: palmr
|
||||
environment:
|
||||
- ENABLE_S3=false
|
||||
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
|
||||
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY (REQUIRED if DISABLE_FILESYSTEM_ENCRYPTION is false)
|
||||
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||
# - DISABLE_FILESYSTEM_ENCRYPTION=true # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional) | (OPTIONAL - default is false)
|
||||
ports:
|
||||
- "5487:5487" # Web port
|
||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
||||
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
|
||||
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
|
||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
volumes:
|
||||
- palmr_data:/app/server # Volume for the application data (changed from /data to /app/server)
|
||||
restart: unless-stopped # Restart the container unless it is stopped
|
||||
|
@@ -20,7 +20,7 @@ environment=PORT=3333,HOME="/home/palmr"
|
||||
priority=100
|
||||
|
||||
[program:web]
|
||||
command=/bin/sh -c 'echo "Waiting for API to be ready..."; while ! curl -f http://127.0.0.1:3333/health >/dev/null 2>&1; do echo "API not ready, waiting..."; sleep 2; done; echo "API is ready! Starting frontend..."; exec node server.js'
|
||||
command=/bin/sh -c 'echo "Please wait while Palmr. is starting..."; while ! curl -f http://127.0.0.1:3333/health >/dev/null 2>&1; do echo "1/2 - Palmr. starting, be patient..."; sleep 2; done; echo "2/2 - Palmr. starting, be patient..."; exec node server.js'
|
||||
directory=/app/web
|
||||
user=palmr
|
||||
autostart=true
|
||||
|
Reference in New Issue
Block a user