Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-sites-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"medialit": minor
---

Uploads are temporary by default
4 changes: 4 additions & 0 deletions .migrations/00005-migrate-to-dual-bucket-architecture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
db.media.updateMany(
{ accessControl: "public-read" },
{ $set: { accessControl: "public" } },
);
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
dist
**/.next
apps/docs/.source
apps/docs/.source
apps/docs/out
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Before you start uploading to your bucket, make sure you have set up the correct
If you need to use a Cloudfront CDN, you can enable it in the app, by setting up the following values in your .env file.

```sh
USE_CLOUDFRONT=true
CLOUDFRONT_ENDPOINT=CLOUDFRONT_DISTRIBUTION_NAME
ACCESS_PRIVATE_BUCKET_VIA_CLOUDFRONT=true
CDN_ENDPOINT=CLOUDFRONT_DISTRIBUTION_NAME
CLOUDFRONT_PRIVATE_KEY="PRIVATE_KEY"
CLOUDFRONT_KEY_PAIR_ID=KEY_PAIR_ID
```
Expand Down
2 changes: 1 addition & 1 deletion apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-slim AS base
FROM node:24-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
Expand Down
348 changes: 348 additions & 0 deletions apps/api/__tests__/media/utils/get-public-urls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
import { describe, test, beforeEach, afterEach } from "node:test";
import assert from "node:assert";
import { Constants, Media } from "@medialit/models";

// Helper to clear module cache and re-import
const clearModuleCache = () => {
const modulePath = require.resolve("@/media/utils/get-public-urls");
const constantsPath = require.resolve("@/config/constants");
delete require.cache[modulePath];
delete require.cache[constantsPath];
};

// Helper to create mock media
const createMockMedia = (overrides: Partial<Media> = {}): Media => ({
fileName: "main.jpg",
mediaId: "test-media-id-123",
apikey: "test-apikey",
originalFileName: "original.jpg",
mimeType: "image/jpeg",
size: 1024,
thumbnailGenerated: true,
accessControl: Constants.AccessControl.PUBLIC,
...overrides,
});

describe("get-public-urls", () => {
const originalEnv: Record<string, string | undefined> = {};

beforeEach(() => {
// Save original env vars
originalEnv.CDN_ENDPOINT = process.env.CDN_ENDPOINT;
originalEnv.CLOUD_ENDPOINT = process.env.CLOUD_ENDPOINT;
originalEnv.CLOUD_ENDPOINT_PUBLIC = process.env.CLOUD_ENDPOINT_PUBLIC;
originalEnv.PATH_PREFIX = process.env.PATH_PREFIX;
});

afterEach(() => {
// Restore original env vars
if (originalEnv.CDN_ENDPOINT !== undefined) {
process.env.CDN_ENDPOINT = originalEnv.CDN_ENDPOINT;
} else {
delete process.env.CDN_ENDPOINT;
}
if (originalEnv.CLOUD_ENDPOINT !== undefined) {
process.env.CLOUD_ENDPOINT = originalEnv.CLOUD_ENDPOINT;
} else {
delete process.env.CLOUD_ENDPOINT;
}
if (originalEnv.CLOUD_ENDPOINT_PUBLIC !== undefined) {
process.env.CLOUD_ENDPOINT_PUBLIC =
originalEnv.CLOUD_ENDPOINT_PUBLIC;
} else {
delete process.env.CLOUD_ENDPOINT_PUBLIC;
}
if (originalEnv.PATH_PREFIX !== undefined) {
process.env.PATH_PREFIX = originalEnv.PATH_PREFIX;
} else {
delete process.env.PATH_PREFIX;
}
clearModuleCache();
});

describe("getPublicFileUrl", () => {
test("should use CDN_ENDPOINT when provided (takes precedence)", () => {
process.env.CDN_ENDPOINT = "https://cdn.example.com";
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
fileName: "main.jpg",
});

const url = getPublicFileUrl(media);
assert.strictEqual(
url,
`https://cdn.example.com/${Constants.PathKey.PUBLIC}/test-media-id-123/main.jpg`,
);
});

test("should use CLOUD_ENDPOINT_PUBLIC when CDN_ENDPOINT not provided and media is public", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
fileName: "main.png",
});

const url = getPublicFileUrl(media);
assert.strictEqual(
url,
`https://public.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/main.png`,
);
});

test("should fallback to CLOUD_ENDPOINT when CLOUD_ENDPOINT_PUBLIC not provided and media is public", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
delete process.env.CLOUD_ENDPOINT_PUBLIC;
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
fileName: "main.webp",
});

const url = getPublicFileUrl(media);
assert.strictEqual(
url,
`https://private.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/main.webp`,
);
});

test("should use CLOUD_ENDPOINT when media is private (defensive check)", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PRIVATE,
fileName: "main.jpg",
});

const url = getPublicFileUrl(media);
assert.strictEqual(
url,
`https://private.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/main.jpg`,
);
});

test("should include PATH_PREFIX when provided", () => {
process.env.CDN_ENDPOINT = "https://cdn.example.com";
process.env.PATH_PREFIX = "tenant-123";
clearModuleCache();

const {
getPublicFileUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
fileName: "main.jpg",
});

const url = getPublicFileUrl(media);
assert.strictEqual(
url,
`https://cdn.example.com/tenant-123/${Constants.PathKey.PUBLIC}/test-media-id-123/main.jpg`,
);
});
});

describe("getThumbnailUrl", () => {
test("should use CDN_ENDPOINT when provided (takes precedence)", () => {
process.env.CDN_ENDPOINT = "https://cdn.example.com";
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia();

const url = getThumbnailUrl(media);
assert.strictEqual(
url,
`https://cdn.example.com/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});

test("should use CLOUD_ENDPOINT_PUBLIC when CDN_ENDPOINT not provided", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia();

const url = getThumbnailUrl(media);
assert.strictEqual(
url,
`https://public.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});

test("should use CLOUD_ENDPOINT_PUBLIC when CDN_ENDPOINT not provided (validation ensures it exists)", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT = "https://private.s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia();

const url = getThumbnailUrl(media);
assert.strictEqual(
url,
`https://public.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});

test("should include PATH_PREFIX when provided", () => {
process.env.CDN_ENDPOINT = "https://cdn.example.com";
process.env.PATH_PREFIX = "tenant-456";
clearModuleCache();

const {
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia();

const url = getThumbnailUrl(media);
assert.strictEqual(
url,
`https://cdn.example.com/tenant-456/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});
});

describe("Progressive behavior", () => {
test("Scenario 1: Base setup - CLOUD_ENDPOINT + CLOUD_ENDPOINT_PUBLIC (no CDN)", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT =
"https://private.r2.cloudflarestorage.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.r2.cloudflarestorage.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
});

const mainUrl = getPublicFileUrl(media);
const thumbUrl = getThumbnailUrl(media);

assert.strictEqual(
mainUrl,
`https://public.r2.cloudflarestorage.com/${Constants.PathKey.PUBLIC}/test-media-id-123/main.jpg`,
);
assert.strictEqual(
thumbUrl,
`https://public.r2.cloudflarestorage.com/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});

test("Scenario 2: With CDN_ENDPOINT (progressive enhancement)", () => {
process.env.CDN_ENDPOINT = "https://cdn.medialit.cloud";
process.env.CLOUD_ENDPOINT =
"https://private.r2.cloudflarestorage.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.r2.cloudflarestorage.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
});

const mainUrl = getPublicFileUrl(media);
const thumbUrl = getThumbnailUrl(media);

// CDN should take precedence
assert.strictEqual(
mainUrl,
`https://cdn.medialit.cloud/${Constants.PathKey.PUBLIC}/test-media-id-123/main.jpg`,
);
assert.strictEqual(
thumbUrl,
`https://cdn.medialit.cloud/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});

test("Scenario 3: CLOUD_ENDPOINT + CLOUD_ENDPOINT_PUBLIC (no CDN, validation ensures both exist)", () => {
delete process.env.CDN_ENDPOINT;
process.env.CLOUD_ENDPOINT = "https://s3.amazonaws.com";
process.env.CLOUD_ENDPOINT_PUBLIC =
"https://public.s3.amazonaws.com";
process.env.PATH_PREFIX = "";
clearModuleCache();

const {
getPublicFileUrl,
getThumbnailUrl,
} = require("@/media/utils/get-public-urls");
const media = createMockMedia({
accessControl: Constants.AccessControl.PUBLIC,
});

const mainUrl = getPublicFileUrl(media);
const thumbUrl = getThumbnailUrl(media);

// Main files use CLOUD_ENDPOINT_PUBLIC for public media
assert.strictEqual(
mainUrl,
`https://public.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/main.jpg`,
);
// Thumbnails always use CLOUD_ENDPOINT_PUBLIC
assert.strictEqual(
thumbUrl,
`https://public.s3.amazonaws.com/${Constants.PathKey.PUBLIC}/test-media-id-123/thumb.webp`,
);
});
});
});
Loading
Loading