mirror of
https://github.com/kyantech/Palmr.git
synced 2025-11-02 13:03:15 +00:00
Compare commits
14 Commits
v3.1.1-bet
...
v3.1.2-bet
Author | SHA1 | Date | |
---|---|---|---|
|
fd28445680 | ||
|
19b7448c3a | ||
|
53c39135af | ||
|
b9147038e6 | ||
|
9a0b7f5c55 | ||
|
2a5f9f03ae | ||
|
78f6e36fc9 | ||
|
8e7aadd183 | ||
|
794a2782ac | ||
|
383f26e777 | ||
|
2db88d3902 | ||
|
5e96633a1e | ||
|
6c80ad8b2a | ||
|
96bd39eb25 |
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-docs",
|
||||
"version": "v3.1.1-beta",
|
||||
"version": "3.1.2-beta",
|
||||
"description": "Docs for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
LayoutIcon,
|
||||
LockIcon,
|
||||
MousePointer,
|
||||
RadioIcon,
|
||||
RocketIcon,
|
||||
SearchIcon,
|
||||
TimerIcon,
|
||||
@@ -79,7 +82,23 @@ function Hero() {
|
||||
<Link href={docsLink}>Documentation</Link>
|
||||
</div>
|
||||
</PulsatingButton>
|
||||
<RippleButton
|
||||
onClick={() => {
|
||||
const demoId = `${Math.random().toString(36).substr(2, 9)}`;
|
||||
const token = `${Math.random().toString(36).substr(2, 12)}`;
|
||||
|
||||
sessionStorage.setItem("demo_token", token);
|
||||
sessionStorage.setItem("demo_id", demoId);
|
||||
sessionStorage.setItem("demo_expires", (Date.now() + 5 * 60 * 1000).toString());
|
||||
|
||||
window.location.href = `/demo?id=${demoId}&token=${token}`;
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<RadioIcon size={18} />
|
||||
Live Demo
|
||||
</div>
|
||||
</RippleButton>
|
||||
<RippleButton>
|
||||
<a
|
||||
href="https://github.com/kyantech/Palmr"
|
||||
|
225
apps/docs/src/app/demo/components/demo-client.tsx
Normal file
225
apps/docs/src/app/demo/components/demo-client.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Palmtree } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import { BackgroundLights } from "@/components/ui/background-lights";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface DemoStatus {
|
||||
status: "waiting" | "ready";
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
interface CreateDemoResponse {
|
||||
message: string;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
function DemoClientInner() {
|
||||
const searchParams = useSearchParams();
|
||||
const demoId = searchParams.get("id");
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const [status, setStatus] = useState<DemoStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const validateAccess = () => {
|
||||
const storedToken = sessionStorage.getItem("demo_token");
|
||||
const storedId = sessionStorage.getItem("demo_id");
|
||||
const expiresAt = sessionStorage.getItem("demo_expires");
|
||||
|
||||
if (!demoId || !token || !storedToken || !storedId || !expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (token !== storedToken || demoId !== storedId || Date.now() > parseInt(expiresAt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!validateAccess()) {
|
||||
setError("Unauthorized access. Please use the Live Demo button to access this page.");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const createDemo = async () => {
|
||||
try {
|
||||
const response = await fetch("https://palmr-demo-manager.kyantech.com.br/create-demo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
palmr_demo_instance_id: demoId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create demo");
|
||||
}
|
||||
|
||||
const data: CreateDemoResponse = await response.json();
|
||||
console.log("Demo creation response:", data);
|
||||
} catch (err) {
|
||||
console.error("Error creating demo:", err);
|
||||
setError("Failed to create demo. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://palmr-demo-manager.kyantech.com.br/status/${demoId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to check demo status");
|
||||
}
|
||||
|
||||
const data: DemoStatus = await response.json();
|
||||
setStatus(data);
|
||||
|
||||
if (data.status === "ready" && data.url) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error checking status:", err);
|
||||
setError("Failed to check demo status. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
createDemo();
|
||||
|
||||
const interval = setInterval(checkStatus, 5000); // Check every 5 seconds
|
||||
|
||||
checkStatus();
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
sessionStorage.removeItem("demo_token");
|
||||
sessionStorage.removeItem("demo_id");
|
||||
sessionStorage.removeItem("demo_expires");
|
||||
};
|
||||
}, [demoId, token]);
|
||||
|
||||
const handleGoToDemo = () => {
|
||||
if (status?.url) {
|
||||
window.open(status.url, "_blank");
|
||||
}
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background">
|
||||
<BackgroundLights />
|
||||
<div className="relative flex flex-col items-center justify-center h-full">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<h1 className="text-2xl font-bold text-destructive">Error</h1>
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
sessionStorage.removeItem("demo_token");
|
||||
sessionStorage.removeItem("demo_id");
|
||||
sessionStorage.removeItem("demo_expires");
|
||||
window.location.href = "/";
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background">
|
||||
<BackgroundLights />
|
||||
<div className="flex flex-col items-center gap-6 text-center h-full justify-center">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold">Your demo is being generated, please wait...</h1>
|
||||
<p className="text-muted-foreground max-w-lg">
|
||||
This demo will be available for 30 minutes for testing. After that, all data will be permanently deleted
|
||||
and become inaccessible. You can test Palmr. with a 200MB storage limit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background">
|
||||
<BackgroundLights />
|
||||
<div className="relative flex flex-col items-center justify-center h-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="container mx-auto max-w-7xl px-6 flex-grow"
|
||||
>
|
||||
<section className="relative flex flex-col items-center justify-center gap-6 m-auto h-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="inline-block max-w-xl text-center justify-center"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.5 }}
|
||||
className="text-4xl lg:text-3xl font-semibold tracking-tight text-primary"
|
||||
>
|
||||
Your demo is ready!
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.6, duration: 0.5 }}
|
||||
className="text-3xl leading-9 font-semibold tracking-tight"
|
||||
>
|
||||
Click the button below to test
|
||||
</motion.span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
className="flex flex-col items-center gap-6"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1.2, duration: 0.5 }}
|
||||
>
|
||||
<Button onClick={handleGoToDemo} className="flex items-center gap-2 px-8 py-4 text-lg">
|
||||
<Palmtree className="h-5 w-5" />
|
||||
Go to Palmr. Demo
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DemoClient() {
|
||||
return <DemoClientInner />;
|
||||
}
|
13
apps/docs/src/app/demo/page.tsx
Normal file
13
apps/docs/src/app/demo/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense } from "react";
|
||||
|
||||
import DemoClient from "./components/demo-client";
|
||||
|
||||
export default function DemoPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<DemoClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
33
apps/docs/src/components/ui/background-lights.tsx
Normal file
33
apps/docs/src/components/ui/background-lights.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export function BackgroundLights() {
|
||||
return (
|
||||
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [0.3, 0.5, 0.3],
|
||||
}}
|
||||
className="absolute -top-[20%] -left-[20%] w-[140%] h-[140%] bg-[radial-gradient(circle,rgba(34,197,94,0.15)_0%,transparent_70%)] dark:opacity-100 opacity-50"
|
||||
transition={{
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [0.3, 0.5, 0.3],
|
||||
}}
|
||||
className="absolute -bottom-[20%] -right-[20%] w-[140%] h-[140%] bg-[radial-gradient(circle,rgba(34,197,94,0.15)_0%,transparent_70%)] dark:opacity-100 opacity-50"
|
||||
transition={{
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 2.5,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
> palmr-api@3.1-beta lint /Users/daniel/clones/Palmr/apps/server
|
||||
> eslint "src/**/*.+(ts|tsx)"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-api",
|
||||
"version": "v3.1.1-beta",
|
||||
"version": "3.1.2-beta",
|
||||
"description": "API for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
@@ -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(),
|
||||
@@ -13,6 +14,7 @@ const envSchema = z.object({
|
||||
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"),
|
||||
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);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { env } from "../../env";
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { ConfigService } from "../config/service";
|
||||
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({
|
||||
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({
|
||||
where: { userId },
|
||||
|
@@ -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);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "../../env";
|
||||
import { FileService } from "../file/service";
|
||||
import {
|
||||
CreateReverseShareInput,
|
||||
@@ -513,7 +514,18 @@ export class ReverseShareService {
|
||||
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({
|
||||
where: { userId: creatorId },
|
||||
select: { size: true },
|
||||
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
||||
import { promisify } from "util";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "../../env";
|
||||
import { IS_RUNNING_IN_CONTAINER } from "../../utils/container-detection";
|
||||
import { ConfigService } from "../config/service";
|
||||
|
||||
@@ -21,6 +22,30 @@ export class StorageService {
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
private _parseSize(value: string): number {
|
||||
if (!value) return 0;
|
||||
|
||||
const cleanValue = value.trim().toLowerCase();
|
||||
|
||||
const numericMatch = cleanValue.match(/^(\d+(?:\.\d+)?)/);
|
||||
if (!numericMatch) return 0;
|
||||
|
||||
const numericValue = parseFloat(numericMatch[1]);
|
||||
if (Number.isNaN(numericValue)) return 0;
|
||||
|
||||
if (cleanValue.includes("t")) {
|
||||
return Math.round(numericValue * 1024 * 1024 * 1024 * 1024);
|
||||
} else if (cleanValue.includes("g")) {
|
||||
return Math.round(numericValue * 1024 * 1024 * 1024);
|
||||
} else if (cleanValue.includes("m")) {
|
||||
return Math.round(numericValue * 1024 * 1024);
|
||||
} else if (cleanValue.includes("k")) {
|
||||
return Math.round(numericValue * 1024);
|
||||
} else {
|
||||
return Math.round(numericValue);
|
||||
}
|
||||
}
|
||||
|
||||
private async _tryDiskSpaceCommand(command: string): Promise<{ total: number; available: number } | null> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
@@ -54,16 +79,61 @@ export class StorageService {
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else if (command.includes("stat -f")) {
|
||||
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;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} 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;
|
||||
total = this._safeParseInt(sizeStr) * 1024;
|
||||
}
|
||||
}
|
||||
} 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")) {
|
||||
total = this._parseSize(size);
|
||||
available = this._parseSize(avail);
|
||||
} else {
|
||||
total = this._safeParseInt(size) * 1024;
|
||||
available = this._safeParseInt(avail) * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,18 +142,13 @@ export class StorageService {
|
||||
if (total > 0 && available >= 0) {
|
||||
return { total, available };
|
||||
} else {
|
||||
console.warn(`Invalid values parsed: total=${total}, available=${available}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Command failed: ${command}`, error);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets detailed mount information for debugging
|
||||
*/
|
||||
private async _getMountInfo(path: string): Promise<{ filesystem: string; mountPoint: string; type: string } | null> {
|
||||
try {
|
||||
if (!fs.existsSync("/proc/mounts")) {
|
||||
@@ -109,16 +174,11 @@ export class StorageService {
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
} catch (error) {
|
||||
console.warn(`Could not get mount info for ${path}:`, error);
|
||||
} catch {
|
||||
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> {
|
||||
try {
|
||||
if (!fs.existsSync("/proc/mounts")) {
|
||||
@@ -133,9 +193,8 @@ export class StorageService {
|
||||
|
||||
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;
|
||||
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
|
||||
bestMatch = mountPoint;
|
||||
bestMatchLength = mountPoint.length;
|
||||
@@ -148,24 +207,16 @@ export class StorageService {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn(`Could not detect mount point for ${path}:`, error);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets filesystem information for a specific path, with bind mount detection
|
||||
*/
|
||||
private async _getFileSystemInfo(
|
||||
path: string
|
||||
): Promise<{ total: number; available: number; mountPoint?: string } | null> {
|
||||
try {
|
||||
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 targetPath = mountPoint || path;
|
||||
|
||||
@@ -174,7 +225,18 @@ 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}"`];
|
||||
: [
|
||||
`df -B1 "${targetPath}"`,
|
||||
`df -k "${targetPath}"`,
|
||||
`df "${targetPath}"`,
|
||||
`df -h "${targetPath}"`,
|
||||
`df -T "${targetPath}"`,
|
||||
`stat -f "${targetPath}"`,
|
||||
`findmnt -n -o AVAIL,SIZE "${targetPath}"`,
|
||||
`findmnt -n -o AVAIL,SIZE,TARGET "${targetPath}"`,
|
||||
`df -P "${targetPath}"`,
|
||||
`df --output=avail,size "${targetPath}"`,
|
||||
];
|
||||
|
||||
for (const command of commandsToTry) {
|
||||
const result = await this._tryDiskSpaceCommand(command);
|
||||
@@ -187,25 +249,54 @@ export class StorageService {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn(`Error getting filesystem info for ${path}:`, error);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (mountPoint.match(/^\/volume\d+$/)) {
|
||||
synologyPaths.push(mountPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return synologyPaths;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
|
||||
const pathsToTry = IS_RUNNING_IN_CONTAINER
|
||||
? ["/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()];
|
||||
|
||||
const synologyPaths = await this._detectSynologyVolumes();
|
||||
|
||||
const pathsToTry = [...basePaths, ...synologyPaths];
|
||||
|
||||
for (const pathToCheck of pathsToTry) {
|
||||
if (pathToCheck.includes("uploads")) {
|
||||
if (pathToCheck.includes("uploads") || pathToCheck.includes("temp-")) {
|
||||
try {
|
||||
if (!fs.existsSync(pathToCheck)) {
|
||||
fs.mkdirSync(pathToCheck, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Could not create path ${pathToCheck}:`, err);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -214,12 +305,8 @@ export class StorageService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the new filesystem detection method
|
||||
const result = await this._getFileSystemInfo(pathToCheck);
|
||||
if (result) {
|
||||
if (result.mountPoint) {
|
||||
console.log(`✅ Storage resolved via bind mount: ${result.mountPoint}`);
|
||||
}
|
||||
return { total: result.total, available: result.available };
|
||||
}
|
||||
}
|
||||
@@ -237,52 +324,94 @@ export class StorageService {
|
||||
uploadAllowed: boolean;
|
||||
}> {
|
||||
try {
|
||||
const isDemoMode = env.DEMO_MODE === "true";
|
||||
|
||||
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) {
|
||||
console.error("❌ Could not determine disk space - system configuration issue");
|
||||
throw new Error("Unable to determine actual disk space - system configuration issue");
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
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) {
|
||||
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
|
||||
if (isDemoMode) {
|
||||
const demoMaxStorage = 200 * 1024 * 1024;
|
||||
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
select: { size: true },
|
||||
});
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
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);
|
||||
const availableStorageGB = this._ensureNumber(maxStorageGB - usedStorageGB, 0);
|
||||
return {
|
||||
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 {
|
||||
diskSizeGB: Number(maxStorageGB.toFixed(2)),
|
||||
diskUsedGB: Number(usedStorageGB.toFixed(2)),
|
||||
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
|
||||
uploadAllowed: availableStorageGB > 0,
|
||||
};
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
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(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");
|
||||
} catch (error) {
|
||||
console.error("❌ Error getting disk space:", error);
|
||||
console.error("Error getting disk space:", error);
|
||||
throw new Error(
|
||||
`Failed to get disk space information: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
|
@@ -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)"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-web",
|
||||
"version": "v3.1.1-beta",
|
||||
"version": "3.1.2-beta",
|
||||
"description": "Frontend for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
@@ -104,8 +104,10 @@ export function EditProviderForm({
|
||||
{ pattern: "linkedin.com", scopes: ["r_liteprofile", "r_emailaddress"] },
|
||||
{ pattern: "auth0.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: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "pocketid", scopes: ["openid", "profile", "email"] },
|
||||
];
|
||||
|
||||
for (const { pattern, scopes } of providerPatterns) {
|
||||
|
@@ -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
|
||||
|
@@ -86,6 +86,19 @@ else
|
||||
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');
|
||||
} catch (error) {
|
||||
console.log('true');
|
||||
@@ -117,6 +130,19 @@ else
|
||||
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');
|
||||
} catch (error) {
|
||||
console.log('true');
|
||||
@@ -132,6 +158,72 @@ else
|
||||
|
||||
if [ "$NEEDS_SEEDING" = "true" ]; then
|
||||
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
|
||||
su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
|
||||
else
|
||||
|
@@ -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