diff --git a/.env.example b/.env.example index 4b16cc8e0..929db566c 100644 --- a/.env.example +++ b/.env.example @@ -63,7 +63,7 @@ SMTP_FROM="Reactive Resume " SMTP_SECURE="false" # --- Storage (optional) --- -# If all keys are disabled, the app uses local filesystem (/data) to store uploads instead. +# If all S3 keys are disabled, the app uses local filesystem storage instead. # Make sure to mount this directory to a volume or the host filesystem to ensure data integrity. # Seaweedfs diff --git a/docs/self-hosting/docker.mdx b/docs/self-hosting/docker.mdx index f996a5c78..cc241694e 100644 --- a/docs/self-hosting/docker.mdx +++ b/docs/self-hosting/docker.mdx @@ -105,7 +105,7 @@ SMTP_FROM="Reactive Resume " SMTP_SECURE="false" # --- Storage (optional) --- -# If all keys are disabled, the app uses local filesystem (usually /app/data) to store uploads instead. +# If all S3 keys are disabled, the app uses local filesystem storage instead. # Make sure to mount this directory to a volume or the host filesystem to ensure data integrity. S3_ACCESS_KEY_ID="" S3_SECRET_ACCESS_KEY="" @@ -327,8 +327,9 @@ openssl rand -hex 32 - - **Default (local)**: If all `S3_*` values are empty, uploads are stored under `/data` (usually `/app/data` in the official image). + - **Default (local)**: If all `S3_*` values are empty, uploads are stored under `/app/data` in the official image. - Mount local uploads to persistent storage (for example `./data:/app/data`) or uploads can be lost on container recreation. + - **Rootless Docker**: `/app/data` remains the container path. Prefer the named volume from the example Compose file, or make sure a bind-mounted host directory is writable by the container's `node` user mapping. - **S3/S3-compatible**: Configure `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`, `S3_ENDPOINT`, and `S3_BUCKET`. - **`S3_FORCE_PATH_STYLE`** controls bucket addressing (defaults to `"false"`): - `"true"` for path-style URLs (`https://endpoint/bucket`) common with MinIO/SeaweedFS. diff --git a/packages/api/src/helpers/local-data-directory.ts b/packages/api/src/helpers/local-data-directory.ts new file mode 100644 index 000000000..871cd7619 --- /dev/null +++ b/packages/api/src/helpers/local-data-directory.ts @@ -0,0 +1,12 @@ +import { basename, dirname, join } from "node:path"; + +export function getLocalDataDirectory(): string { + const cwd = process.cwd(); + const parentDirectory = dirname(cwd); + + if (basename(cwd) === "web" && basename(parentDirectory) === "apps") { + return join(dirname(parentDirectory), "data"); + } + + return join(cwd, "data"); +} diff --git a/packages/api/src/services/resume-access-policy.ts b/packages/api/src/helpers/resume-access-policy.ts similarity index 100% rename from packages/api/src/services/resume-access-policy.ts rename to packages/api/src/helpers/resume-access-policy.ts diff --git a/packages/api/src/services/resume.ts b/packages/api/src/services/resume.ts index 800e3c5ea..f8a8dfc36 100644 --- a/packages/api/src/services/resume.ts +++ b/packages/api/src/services/resume.ts @@ -13,7 +13,12 @@ import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { applyResumePatches, ResumePatchError } from "@reactive-resume/utils/resume/patch"; import { generateId } from "@reactive-resume/utils/string"; import { grantResumeAccess, hasResumeAccess } from "../helpers/resume-access"; -import { assertCanView, isOwner, redactResumeForViewer, shouldCountForStatistics } from "./resume-access-policy"; +import { + assertCanView, + isOwner, + redactResumeForViewer, + shouldCountForStatistics, +} from "../helpers/resume-access-policy"; import { getStorageService } from "./storage"; const tags = { diff --git a/packages/api/src/services/statistics.ts b/packages/api/src/services/statistics.ts index a5508cf0a..663079126 100644 --- a/packages/api/src/services/statistics.ts +++ b/packages/api/src/services/statistics.ts @@ -3,6 +3,7 @@ import { dirname, join } from "node:path"; import { count } from "drizzle-orm"; import { db } from "@reactive-resume/db/client"; import * as schema from "@reactive-resume/db/schema"; +import { getLocalDataDirectory } from "../helpers/local-data-directory"; const CACHE_DURATION_MS = 6 * 60 * 60 * 1000; // 6 hours const GITHUB_API_URL = "https://api.github.com/repos/amruthpillai/reactive-resume"; @@ -15,7 +16,7 @@ const LAST_KNOWN = { stars: 34_073, } as const; -const getCachePath = (key: string) => join(process.cwd(), "data", "statistics", `${key}.txt`); +const getCachePath = (key: string) => join(getLocalDataDirectory(), "statistics", `${key}.txt`); const readCache = async (key: string): Promise => { try { diff --git a/packages/api/src/services/storage.ts b/packages/api/src/services/storage.ts index ebac8ec8a..7cc358fc9 100644 --- a/packages/api/src/services/storage.ts +++ b/packages/api/src/services/storage.ts @@ -9,6 +9,7 @@ import { } from "@aws-sdk/client-s3"; import sharp from "sharp"; import { env } from "@reactive-resume/env/server"; +import { getLocalDataDirectory } from "../helpers/local-data-directory"; interface StorageWriteInput { key: string; @@ -114,7 +115,7 @@ class LocalStorageService implements StorageService { private rootDirectory: string; constructor() { - this.rootDirectory = join(process.cwd(), "data"); + this.rootDirectory = getLocalDataDirectory(); } async list(prefix: string): Promise {