Created
December 14, 2025 14:11
-
-
Save Cyang39/7ecb68e2ee2f6ffc644977f10d9af4e1 to your computer and use it in GitHub Desktop.
feather wiki nests server bun version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bun | |
| // == Introduction | |
| // Source: https://codeberg.org/Alamantus/FeatherWiki/src/branch/main/nests/server.mjs | |
| // Why bun version? Because running the original Node script with Bun causes crashes. | |
| // Tip1: Run `bun run server.mjs` | |
| // Tip2: Run `docker run -d -p 4505:4505 -v "$PWD":/app -w /app oven/bun:latest bun run server.mjs` | |
| // == Introduction End | |
| // == Configuration | |
| const userpass = null; | |
| // use the line below instead to enable basic authentication | |
| // const userpass = "user:pass"; | |
| // where the Feather Wiki file is located | |
| const wikifile = "index.html"; | |
| const hostname = "0.0.0.0"; | |
| const port = 4505; | |
| // == Configuration End | |
| let mutexId = 0; | |
| const waitingResolvers = []; | |
| let currentMutexId = null; | |
| function acquireMutex() { | |
| if (!currentMutexId) { | |
| currentMutexId = mutexId++; | |
| return Promise.resolve(currentMutexId); | |
| } | |
| return new Promise((resolve) => waitingResolvers.push(resolve)); | |
| } | |
| function releaseMutex(id) { | |
| if (id !== currentMutexId) { | |
| throw new Error(`Release ID doesn't match current lock ID`); | |
| } | |
| if (waitingResolvers.length > 0) { | |
| const resolve = waitingResolvers.shift(); | |
| currentMutexId = mutexId++; | |
| resolve(currentMutexId); | |
| } else { | |
| currentMutexId = null; | |
| } | |
| } | |
| function notAuthorized() { | |
| return new Response(null, { | |
| status: 401, | |
| headers: { | |
| "WWW-Authenticate": `Basic realm="FeatherWiki"`, | |
| }, | |
| }); | |
| } | |
| function checkBasicAuth(request) { | |
| if (userpass == null) return null; | |
| const authorization = request.headers.get("Authorization"); | |
| if (!authorization) return notAuthorized(); | |
| const match = /Basic (.*)/.exec(authorization); | |
| if (!match) return notAuthorized(); | |
| const given_userpass = atob(match[1]); | |
| console.log("Received Authorization:", given_userpass); | |
| if (given_userpass !== userpass) return notAuthorized(); | |
| return null; | |
| } | |
| Bun.serve({ | |
| hostname, | |
| port, | |
| async fetch(request) { | |
| // Basic auth (if enabled) | |
| const authResp = checkBasicAuth(request); | |
| if (authResp) return authResp; | |
| switch (request.method) { | |
| case "GET": { | |
| const id = await acquireMutex(); | |
| try { | |
| const body = await Bun.file(wikifile).text(); | |
| return new Response(body, { | |
| status: 200, | |
| headers: { "Content-Type": "text/html" }, | |
| }); | |
| } finally { | |
| releaseMutex(id); | |
| } | |
| } | |
| case "PUT": { | |
| const id = await acquireMutex(); | |
| try { | |
| const body = await request.text(); | |
| await Bun.write(wikifile, body); | |
| return new Response(null, { status: 204 }); | |
| } finally { | |
| releaseMutex(id); | |
| } | |
| } | |
| case "OPTIONS": { | |
| return new Response(null, { | |
| status: 204, | |
| headers: { | |
| DAV: "1", | |
| Allow: "GET, PUT, OPTIONS", | |
| "Content-Length": "0", | |
| }, | |
| }); | |
| } | |
| default: | |
| return new Response(null, { status: 500 }); | |
| } | |
| }, | |
| }); | |
| console.log(`Server running at http://${hostname}:${port}/`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment