Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a04de9a4f | |||
| 66168e95cc | |||
| 17d01aff21 | |||
| 9b44356782 | |||
| a035162a6b | |||
| a19073d1f0 | |||
| 10abe4a31f | |||
| af6d70fe6e |
@@ -71,7 +71,9 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
tags: |
|
||||||
|
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||||
|
${{ env.IMAGE_NAME }}:latest
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||||
org.opencontainers.image.revision=${{ github.sha }}
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
# excali
|
# excali-box
|
||||||
|
|
||||||
Private self-hosted Excalidraw with Bun and SQLite.
|
Private self-hosted Excalidraw with Bun and SQLite.
|
||||||
|
|
||||||
|
This is bare wrapper around the excalidraw packages that adds autosave and disk-persistence.
|
||||||
|
No auth to keep things simple - you either run in a private network or put it behind a Caddy auth or similar solutions.
|
||||||
|
|
||||||
|
> GPT 5.5 generated
|
||||||
|
|
||||||
|
## Pull from GHCR and run
|
||||||
|
|
||||||
|
Images are published to:
|
||||||
|
|
||||||
|
`ghcr.io/ruinivist/excalidraw-box:<version>`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/ruinivist/excalidraw-box:latest
|
||||||
|
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD/excali-box-data:/data" ghcr.io/ruinivist/excalidraw-box:latest
|
||||||
|
```
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -22,14 +38,7 @@ bun run typecheck
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t excali .
|
docker build -t excali .
|
||||||
docker run --rm -p 3000:3000 -v "$PWD/excali-box-data:/data" excali
|
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD/excali-box-data:/data" excali
|
||||||
```
|
```
|
||||||
|
|
||||||
This will make the data persistent in the `excali-box-data` folder in the current directory, the site will be available at `localhost:3000`.
|
This will make the data persistent in the `excali-box-data` folder in the current directory, the site will be available at `localhost:3000`.
|
||||||
|
|
||||||
> Note that this command will bind to ALL interfaces. If you use a rev
|
|
||||||
> proxy, you may want to bind to locahost instead, you can do that by setting the `HOST` environment variable to `localhost`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -p 3000:3000 -v "$PWD/excali-box-data:/data" -e HOST=localhost excali
|
|
||||||
```
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/server.tsx",
|
"dev": "bun --hot src/server.tsx",
|
||||||
"build": "rm -rf dist && bun build --target=bun ./src/server.tsx --outdir ./dist",
|
"build": "rm -rf dist && bun build --minify --target=browser ./src/index.html --outdir ./dist/public && bun build --target=bun ./src/server.tsx --outdir ./dist",
|
||||||
"start": "cd dist && DATABASE_PATH=../data/excalidraw.sqlite bun ./server.js",
|
"start": "cd dist && DATABASE_PATH=../data/excalidraw.sqlite bun ./server.js",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
|||||||
+48
-3
@@ -8,13 +8,22 @@ export function createServer() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
port: Number(process.env.PORT ?? "3000"),
|
port: Number(process.env.PORT ?? "3000"),
|
||||||
hostname: process.env.HOST ?? "0.0.0.0",
|
hostname: process.env.HOST ?? "localhost",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (request: Request) => {
|
"/": (request: Request) => {
|
||||||
const drawing = drawingStore.ensureInitialDrawing();
|
const drawing = drawingStore.ensureInitialDrawing();
|
||||||
return Response.redirect(new URL(`/d/${drawing.id}`, request.url), 302);
|
return Response.redirect(new URL(`/d/${drawing.id}`, request.url), 302);
|
||||||
},
|
},
|
||||||
"/d/:id": index,
|
...(process.cwd().endsWith("/dist") ? {
|
||||||
|
"/d/:id": async (req: Request) => {
|
||||||
|
let file = Bun.file("./public/index.html");
|
||||||
|
return new Response(file, {
|
||||||
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} : {
|
||||||
|
"/d/:id": index
|
||||||
|
}),
|
||||||
"/api/health": {
|
"/api/health": {
|
||||||
GET: () => api.health(),
|
GET: () => api.health(),
|
||||||
},
|
},
|
||||||
@@ -28,7 +37,43 @@ export function createServer() {
|
|||||||
DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request),
|
DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fetch() {
|
async fetch(request: Request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const isProd = process.cwd().endsWith("/dist");
|
||||||
|
|
||||||
|
if (!isProd) {
|
||||||
|
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any "/public" prefix from the request URL if it accidentally got included
|
||||||
|
let reqPath = url.pathname;
|
||||||
|
if (reqPath.startsWith("/public/")) {
|
||||||
|
reqPath = reqPath.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In dist, static files are inside public
|
||||||
|
const path = "./public" + reqPath;
|
||||||
|
const file = Bun.file(path);
|
||||||
|
if (await file.exists()) {
|
||||||
|
// Cache indefinitely except index.html
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (!reqPath.endsWith("index.html")) {
|
||||||
|
// Add etag logic or use bun's hashes by only caching files that actually have hashes in them.
|
||||||
|
// Bun generates hashes like chunk-02ahh9r5.js, we can check for that pattern
|
||||||
|
// If we wanted to do ETag we could use Bun.file(path).size + lastModified but hashing is safer
|
||||||
|
const isHashed = reqPath.match(/-[a-zA-Z0-9]{8}\.(js|css)$/);
|
||||||
|
if (isHashed) {
|
||||||
|
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
||||||
|
} else {
|
||||||
|
headers["Cache-Control"] = "no-cache";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers["Content-Type"] = "text/html; charset=utf-8";
|
||||||
|
headers["Cache-Control"] = "no-cache";
|
||||||
|
}
|
||||||
|
return new Response(file, { headers });
|
||||||
|
}
|
||||||
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
|
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
},
|
},
|
||||||
} satisfies Parameters<typeof Bun.serve>[0];
|
} satisfies Parameters<typeof Bun.serve>[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user