mirror of
https://github.com/kyantech/Palmr.git
synced 2025-11-01 12:33:34 +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 "Starting Palmr Application..."
|
||||||
echo "Storage Mode: \${ENABLE_S3:-false}"
|
echo "Storage Mode: \${ENABLE_S3:-false}"
|
||||||
echo "Secure Site: \${SECURE_SITE:-false}"
|
echo "Secure Site: \${SECURE_SITE:-false}"
|
||||||
|
echo "Encryption: \${DISABLE_FILESYSTEM_ENCRYPTION:-false}"
|
||||||
echo "Database: SQLite"
|
echo "Database: SQLite"
|
||||||
|
|
||||||
# Set global environment variables
|
# 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:
|
environment:
|
||||||
- ENABLE_S3=false
|
- 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
|
||||||
|
# - 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
|
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||||
ports:
|
ports:
|
||||||
- "5487:5487" # Web interface
|
- "5487:5487" # Web interface
|
||||||
@@ -102,9 +103,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ENABLE_S3=false
|
- 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
|
||||||
# - 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_UID=1000 # UID for the container processes (default is 1001)
|
||||||
- PALMR_GID=1000 # GID 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:
|
ports:
|
||||||
- "5487:5487" # Web port
|
- "5487:5487" # Web port
|
||||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
- "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:
|
Configure Palmr. behavior through environment variables:
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ---------------- | ------- | ------------------------------------------------------- |
|
| ------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
||||||
| `ENABLE_S3` | `false` | Enable S3-compatible storage |
|
| `ENABLE_S3` | `false` | Enable S3-compatible storage |
|
||||||
| `ENCRYPTION_KEY` | - | **Required**: Minimum 32 characters for file encryption |
|
| `ENCRYPTION_KEY` | - | **Required** (unless encryption disabled): Minimum 32 characters for file encryption |
|
||||||
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy setups |
|
| `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.
|
> **🔗 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 />
|
<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.
|
## Accessing Palmr.
|
||||||
@@ -177,6 +184,8 @@ docker run -d \
|
|||||||
--name palmr \
|
--name palmr \
|
||||||
-e ENABLE_S3=false \
|
-e ENABLE_S3=false \
|
||||||
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
|
-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 5487:5487 \
|
||||||
-p 3333:3333 \
|
-p 3333:3333 \
|
||||||
-v palmr_data:/app/server \
|
-v palmr_data:/app/server \
|
||||||
@@ -184,6 +193,8 @@ docker run -d \
|
|||||||
kyantech/palmr:latest
|
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:**
|
**Bind Mount:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -191,6 +202,10 @@ docker run -d \
|
|||||||
--name palmr \
|
--name palmr \
|
||||||
-e ENABLE_S3=false \
|
-e ENABLE_S3=false \
|
||||||
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
|
-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 5487:5487 \
|
||||||
-p 3333:3333 \
|
-p 3333:3333 \
|
||||||
-v $(pwd)/data:/app/server \
|
-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: {
|
logger: {
|
||||||
level: "info",
|
level: "warn",
|
||||||
},
|
},
|
||||||
bodyLimit: 1024 * 1024 * 1024 * 1024 * 1024,
|
bodyLimit: 1024 * 1024 * 1024 * 1024 * 1024,
|
||||||
connectionTimeout: 0,
|
connectionTimeout: 0,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
const envSchema = z.object({
|
const envSchema = z.object({
|
||||||
ENABLE_S3: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
ENABLE_S3: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
||||||
ENCRYPTION_KEY: z.string().optional().default("palmr-default-encryption-key-2025"),
|
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_ENDPOINT: z.string().optional(),
|
||||||
S3_PORT: z.string().optional(),
|
S3_PORT: z.string().optional(),
|
||||||
S3_USE_SSL: 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`);
|
console.log(`Chunk ${chunkIndex} already uploaded, treating as success`);
|
||||||
|
|
||||||
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
|
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
|
||||||
// Check if already finalizing to prevent race condition
|
|
||||||
if (this.finalizingUploads.has(fileId)) {
|
if (this.finalizingUploads.has(fileId)) {
|
||||||
console.log(`Upload ${fileId} is already being finalized, waiting...`);
|
console.log(`Upload ${fileId} is already being finalized, waiting...`);
|
||||||
return { isComplete: false };
|
return { isComplete: false };
|
||||||
@@ -128,7 +127,6 @@ export class ChunkManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
|
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
|
||||||
// Check if already finalizing to prevent race condition
|
|
||||||
if (this.finalizingUploads.has(fileId)) {
|
if (this.finalizingUploads.has(fileId)) {
|
||||||
console.log(`Upload ${fileId} is already being finalized, waiting...`);
|
console.log(`Upload ${fileId} is already being finalized, waiting...`);
|
||||||
return { isComplete: false };
|
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(
|
private async finalizeUpload(
|
||||||
chunkInfo: ChunkInfo,
|
chunkInfo: ChunkInfo,
|
||||||
@@ -224,7 +222,7 @@ export class ChunkManager {
|
|||||||
const filePath = provider.getFilePath(finalObjectName);
|
const filePath = provider.getFilePath(finalObjectName);
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
|
|
||||||
console.log(`Starting encryption and finalization: ${finalObjectName}`);
|
console.log(`Starting finalization: ${finalObjectName}`);
|
||||||
|
|
||||||
await fs.promises.mkdir(dir, { recursive: true });
|
await fs.promises.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
@@ -236,7 +234,6 @@ export class ChunkManager {
|
|||||||
});
|
});
|
||||||
const encryptStream = provider.createEncryptStream();
|
const encryptStream = provider.createEncryptStream();
|
||||||
|
|
||||||
// Wait for encryption to complete BEFORE cleaning up temp file
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
@@ -245,18 +242,17 @@ export class ChunkManager {
|
|||||||
.pipe(writeStream)
|
.pipe(writeStream)
|
||||||
.on("finish", () => {
|
.on("finish", () => {
|
||||||
const duration = Date.now() - startTime;
|
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();
|
resolve();
|
||||||
})
|
})
|
||||||
.on("error", (error) => {
|
.on("error", (error) => {
|
||||||
console.error("Error during encryption:", error);
|
console.error("Error during processing:", error);
|
||||||
reject(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);
|
await this.cleanupTempFile(chunkInfo.tempPath);
|
||||||
|
|
||||||
this.activeUploads.delete(chunkInfo.fileId);
|
this.activeUploads.delete(chunkInfo.fileId);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ 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();
|
||||||
|
|
||||||
@@ -21,6 +23,34 @@ export class StorageService {
|
|||||||
return Number.isNaN(parsed) ? 0 : parsed;
|
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> {
|
private async _tryDiskSpaceCommand(command: string): Promise<{ total: number; available: number } | null> {
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execAsync(command);
|
const { stdout, stderr } = await execAsync(command);
|
||||||
@@ -53,17 +83,79 @@ export class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Handle different Linux/Unix command formats
|
||||||
const lines = stdout.trim().split("\n");
|
const lines = stdout.trim().split("\n");
|
||||||
if (lines.length >= 2) {
|
|
||||||
const parts = lines[1].trim().split(/\s+/);
|
// Handle findmnt command output
|
||||||
if (parts.length >= 4) {
|
if (command.includes("findmnt")) {
|
||||||
const [, size, , avail] = parts;
|
if (lines.length >= 1) {
|
||||||
if (command.includes("-B1")) {
|
const parts = lines[0].trim().split(/\s+/);
|
||||||
total = this._safeParseInt(size);
|
if (parts.length >= 2) {
|
||||||
available = this._safeParseInt(avail);
|
const [availStr, sizeStr] = parts;
|
||||||
} else {
|
available = this._parseSize(availStr);
|
||||||
total = this._safeParseInt(size) * 1024;
|
total = this._parseSize(sizeStr);
|
||||||
available = this._safeParseInt(avail) * 1024;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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) {
|
if (total > 0 && available >= 0) {
|
||||||
return { total, available };
|
return { total, available };
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Invalid values parsed: total=${total}, available=${available}`);
|
console.warn(`Invalid values parsed: total=${total}, available=${available} for command: ${command}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -87,6 +179,7 @@ export class StorageService {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +189,26 @@ 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;
|
||||||
@@ -108,6 +216,10 @@ export class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bestMatch) {
|
||||||
|
console.log(`🎯 Selected mount info: ${bestMatch.filesystem} → ${bestMatch.mountPoint} (${bestMatch.type})`);
|
||||||
|
}
|
||||||
|
|
||||||
return bestMatch;
|
return bestMatch;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Could not get mount info for ${path}:`, 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> {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,22 +244,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 >= 2) {
|
if (parts.length >= 3) {
|
||||||
const [, mountPoint] = 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 (error) {
|
||||||
console.warn(`Could not detect mount point for ${path}:`, error);
|
console.warn(`Could not detect mount point for ${path}:`, error);
|
||||||
@@ -174,11 +297,31 @@ export class StorageService {
|
|||||||
? ["wmic logicaldisk get size,freespace,caption"]
|
? ["wmic logicaldisk get size,freespace,caption"]
|
||||||
: process.platform === "darwin"
|
: process.platform === "darwin"
|
||||||
? [`df -k "${targetPath}"`, `df "${targetPath}"`]
|
? [`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) {
|
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,
|
||||||
@@ -186,6 +329,7 @@ export class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(`❌ All commands failed for path: ${targetPath}`);
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Error getting filesystem info for ${path}:`, 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> {
|
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
|
||||||
const pathsToTry = IS_RUNNING_IN_CONTAINER
|
// Base paths that work for all systems
|
||||||
? ["/app/server/uploads", "/app/server", "/app", "/"]
|
const basePaths = IS_RUNNING_IN_CONTAINER
|
||||||
|
? ["/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();
|
||||||
|
|
||||||
|
// 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) {
|
for (const pathToCheck of pathsToTry) {
|
||||||
if (pathToCheck.includes("uploads")) {
|
console.log(`📁 Checking path: ${pathToCheck}`);
|
||||||
|
|
||||||
|
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 });
|
||||||
@@ -211,6 +400,7 @@ export class StorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(pathToCheck)) {
|
if (!fs.existsSync(pathToCheck)) {
|
||||||
|
console.log(`❌ Path does not exist: ${pathToCheck}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,10 +410,16 @@ export class StorageService {
|
|||||||
if (result.mountPoint) {
|
if (result.mountPoint) {
|
||||||
console.log(`✅ Storage resolved via bind mount: ${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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { pipeline } from "stream/promises";
|
|||||||
import { directoriesConfig, getTempFilePath } from "../config/directories.config";
|
import { directoriesConfig, getTempFilePath } from "../config/directories.config";
|
||||||
import { env } from "../env";
|
import { env } from "../env";
|
||||||
import { StorageProvider } from "../types/storage";
|
import { StorageProvider } from "../types/storage";
|
||||||
import { IS_RUNNING_IN_CONTAINER } from "../utils/container-detection";
|
|
||||||
|
|
||||||
export class FilesystemStorageProvider implements StorageProvider {
|
export class FilesystemStorageProvider implements StorageProvider {
|
||||||
private static instance: FilesystemStorageProvider;
|
private static instance: FilesystemStorageProvider;
|
||||||
private uploadsDir: string;
|
private uploadsDir: string;
|
||||||
private encryptionKey = env.ENCRYPTION_KEY;
|
private encryptionKey = env.ENCRYPTION_KEY;
|
||||||
|
private isEncryptionDisabled = env.DISABLE_FILESYSTEM_ENCRYPTION === "true";
|
||||||
private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
|
private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
|
||||||
private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
|
private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
|
||||||
|
|
||||||
@@ -66,6 +66,15 @@ export class FilesystemStorageProvider implements StorageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public createEncryptStream(): Transform {
|
public createEncryptStream(): Transform {
|
||||||
|
if (this.isEncryptionDisabled) {
|
||||||
|
return new Transform({
|
||||||
|
transform(chunk, encoding, callback) {
|
||||||
|
this.push(chunk);
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const key = this.createEncryptionKey();
|
const key = this.createEncryptionKey();
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||||
@@ -101,6 +110,15 @@ export class FilesystemStorageProvider implements StorageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public createDecryptStream(): Transform {
|
public createDecryptStream(): Transform {
|
||||||
|
if (this.isEncryptionDisabled) {
|
||||||
|
return new Transform({
|
||||||
|
transform(chunk, encoding, callback) {
|
||||||
|
this.push(chunk);
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const key = this.createEncryptionKey();
|
const key = this.createEncryptionKey();
|
||||||
let iv: Buffer | null = null;
|
let iv: Buffer | null = null;
|
||||||
let decipher: crypto.Decipher | 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> {
|
async downloadFile(objectName: string): Promise<Buffer> {
|
||||||
const filePath = this.getFilePath(objectName);
|
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 {
|
try {
|
||||||
return this.decryptFileBuffer(encryptedBuffer);
|
return this.decryptFileBuffer(fileBuffer);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.warn("Failed to decrypt with new method, trying legacy format", error.message);
|
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 {
|
private decryptFileBuffer(encryptedBuffer: Buffer): Buffer {
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🌴 Palmr server running on port 3333 🌴`);
|
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(`🔐 Auth Providers: ${authProviders}`);
|
||||||
|
|
||||||
console.log("\n📚 API Documentation:");
|
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
|
container_name: palmr
|
||||||
environment:
|
environment:
|
||||||
- ENABLE_S3=false
|
- 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 (default is 1001) you can change it to the UID of the user running the container
|
- 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 (default is 1001) you can change it to the GID of the user running the container
|
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
# - 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:
|
ports:
|
||||||
- "5487:5487" # Web port
|
- "5487:5487" # Web port
|
||||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
- "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_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_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
|
- 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_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 (default is 1001) you can change it to the GID of the user running the container
|
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||||
ports:
|
ports:
|
||||||
- "5487:5487" # Web port
|
- "5487:5487" # Web port
|
||||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
- "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_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_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
|
- 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_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 (default is 1001) you can change it to the GID of the user running the container
|
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
|
||||||
- SECURE_SITE=false # Set to true if you are using a reverse proxy
|
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
|
||||||
ports:
|
ports:
|
||||||
- "5487:5487" # Web port
|
- "5487:5487" # Web port
|
||||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
- "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
|
container_name: palmr
|
||||||
environment:
|
environment:
|
||||||
- ENABLE_S3=false
|
- 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:
|
ports:
|
||||||
- "5487:5487" # Web port
|
- "5487:5487" # Web port
|
||||||
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
|
- "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:
|
volumes:
|
||||||
- palmr_data:/app/server # Volume for the application data (changed from /data to /app/server)
|
- 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
|
restart: unless-stopped # Restart the container unless it is stopped
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ environment=PORT=3333,HOME="/home/palmr"
|
|||||||
priority=100
|
priority=100
|
||||||
|
|
||||||
[program:web]
|
[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
|
directory=/app/web
|
||||||
user=palmr
|
user=palmr
|
||||||
autostart=true
|
autostart=true
|
||||||
|
|||||||
Reference in New Issue
Block a user