Skip to content
Open
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
Binary file modified apps/server/spec/db/document.db
Binary file not shown.
94 changes: 94 additions & 0 deletions apps/server/spec/etapi/external-blobs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { login } from "./utils.js";
import config from "../../src/services/config.js";
import sql from "../../src/services/sql.js";

let app: Application;
let token: string;

const USER = "etapi";

describe("etapi/external-blobs", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
config.ExternalBlobStorage.enabled = true;
config.ExternalBlobStorage.thresholdBytes = 10;

const buildApp = (await import("../../src/app.js")).default;
app = await buildApp();
token = await login(app);
});

it("stores small note content internally", async () => {
const payload = "a".repeat(10);
const createRes = await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic"})
.send({
"parentNoteId": "root",
"title": "Internal Blob Test",
"mime": "text/plain",
"type": "text",
"content": payload
})
.expect(201);

const createdNoteId: string = createRes.body.note.noteId;
expect(createdNoteId).toBeTruthy();

const blobId = sql.getValue<string>("SELECT blobId FROM notes WHERE noteId = ?", [createdNoteId]);
expect(blobId).toBeTruthy();

const row = sql.getRow<{ contentLocation: string; content: string | null; contentLength: number }>(
"SELECT contentLocation, content, contentLength FROM blobs WHERE blobId = ?",
[blobId]
);

expect(row).toBeTruthy();
expect(row.contentLength).toEqual(payload.length);
expect(row.contentLocation).toEqual("internal");
expect(row.content).toEqual(payload);
});

it("stores large note content externally and serves it back", async () => {
const payload = "a".repeat(11);
const createRes = await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic"})
.send({
"parentNoteId": "root",
"title": "External Blob Test",
"mime": "application/octet-stream",
"type": "file",
"content": payload
})
.expect(201);

const createdNoteId: string = createRes.body.note.noteId;
expect(createdNoteId).toBeTruthy();

const blobId = sql.getValue<string>("SELECT blobId FROM notes WHERE noteId = ?", [createdNoteId]);
expect(blobId).toBeTruthy();

const row = sql.getRow<{ contentLocation: string; content: string | null; contentLength: number }>(
"SELECT contentLocation, content, contentLength FROM blobs WHERE blobId = ?",
[blobId]
);

expect(row).toBeTruthy();
expect(row.contentLength).toEqual(payload.length);
expect(row.contentLocation.startsWith("file://")).toBe(true);
expect(row.content).toBeNull();

const getRes = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic"})
.expect(200);

expect(getRes.body.toString()).toEqual(payload);
});
});


15 changes: 15 additions & 0 deletions apps/server/src/assets/config-sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,18 @@ oauthIssuerName=
# Set the issuer icon for OAuth/OpenID authentication
# This is the icon of the service that will be used to verify the user's identity
oauthIssuerIcon=

[ExternalBlobStorage]
# External blob storage allows large attachments to be stored as separate files
# instead of being embedded in the SQLite database. This can improve database
# performance and simplify backup strategies for large files.
#
# When enabled=false (default), all attachments are stored in the database
# When enabled=true, attachments larger than thresholdBytes are stored as files
# in the external-blobs directory within your data directory
enabled=false

# Threshold size in bytes - attachments larger than this will be stored externally
# Default is 100KB (102400 bytes), which balances performance and flexibility
# See https://www.sqlite.org/intern-v-extern-blob.html for performance considerations
thresholdBytes=102400
2 changes: 2 additions & 0 deletions apps/server/src/assets/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ CREATE TABLE IF NOT EXISTS "recent_notes"
CREATE TABLE IF NOT EXISTS "blobs" (
`blobId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
`contentLocation` TEXT NOT NULL DEFAULT 'internal',
`contentLength` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`blobId`)
Expand Down

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading