From 9389fd5f8c0e48aaa1d752f346a69711ecdf803b Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 11 May 2025 14:07:03 -0400 Subject: [PATCH] first commit --- .editorconfig | 12 ++++++ .env.example | 11 +++++ .forgejo/workflows/biomejs.yml | 24 +++++++++++ .gitattributes | 1 + .gitignore | 3 ++ Dockerfile | 37 +++++++++++++++++ LICENSE | 28 +++++++++++++ README.md | 70 +++++++++++++++++++++++++++++++ biome.json | 34 +++++++++++++++ compose.yml | 16 +++++++ config/environment.ts | 44 ++++++++++++++++++++ package.json | 24 +++++++++++ src/index.ts | 76 ++++++++++++++++++++++++++++++++++ tsconfig.json | 33 +++++++++++++++ types/config.d.ts | 5 +++ 15 files changed, 418 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .forgejo/workflows/biomejs.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 biome.json create mode 100644 compose.yml create mode 100644 config/environment.ts create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json create mode 100644 types/config.d.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..980ef21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d1c5ed3 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Server configuration +HOST=0.0.0.0 +PORT=8080 + +# Forgejo instance settings +FORGEJO_URL=https://git.example.com +FORGEJO_TOKEN=your_forgejo_api_token_here + +# Repository branch and name to serve static content from +BRANCH=static-pages +REPO=pages diff --git a/.forgejo/workflows/biomejs.yml b/.forgejo/workflows/biomejs.yml new file mode 100644 index 0000000..15c990c --- /dev/null +++ b/.forgejo/workflows/biomejs.yml @@ -0,0 +1,24 @@ +name: Code quality checks + +on: + push: + pull_request: + +jobs: + biome: + runs-on: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + export BUN_INSTALL="$HOME/.bun" + echo "$BUN_INSTALL/bin" >> $GITHUB_PATH + + - name: Install Dependencies + run: bun install + + - name: Run Biome with verbose output + run: bunx biome ci . --verbose diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d23d9c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules +bun.lock +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b048099 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# use the official Bun image +# see all versions at https://hub.docker.com/r/oven/bun/tags +FROM oven/bun:latest AS base +WORKDIR /usr/src/app + +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lock /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +# install with --production (exclude devDependencies) +RUN mkdir -p /temp/prod +COPY package.json bun.lock /temp/prod/ +RUN cd /temp/prod && bun install --frozen-lockfile --production + +# copy node_modules from temp directory +# then copy all (non-ignored) project files into the image +FROM base AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [optional] tests & build +ENV NODE_ENV=production + +# copy production dependencies and source code into final image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/app/src ./src +COPY --from=prerelease /usr/src/app/package.json . +COPY --from=prerelease /usr/src/app/tsconfig.json . +COPY --from=prerelease /usr/src/app/config ./config +COPY --from=prerelease /usr/src/app/types ./types + +RUN mkdir -p /usr/src/app/logs && chown bun:bun /usr/src/app/logs + +USER bun +ENTRYPOINT [ "bun", "run", "start" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c69e3f --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, [fullname] + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c290a30 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Bun Forgejo Pages Proxy + +A simple static page server built with Bun that proxies files from a Forgejo repository. + +## Features + +- Serves static files (HTML, CSS, JS, images, etc.) from a `static-pages` branch +- Automatic `index.html` resolution for directory paths +- Proper MIME type handling via `mime` package +- Minimal and fast Bun server + +## Requirements + +- [Bun](https://bun.sh/) >= 1.2 +- A Forgejo instance with an API token +- Repositories with a `static-pages` branch (or any configured via `BRANCH`) + +## Setup + +### 1. Clone the repository + +```bash +git clone https://git.creations.works/creations/forgejoPages +cd forgejoPages +``` + +### 2. Install dependencies + +```bash +bun install +``` + +### 3. Configure environment + +Copy `.env.example` to `.env` and fill in your Forgejo details: + +```bash +cp .env.example .env +``` + +### 4. Start the server + +```bash +bun run dev +``` + +## Environment Variables + +| Name | Description | +|-----------------|-----------------------------------------------------| +| `HOST` | Host to bind the server to | +| `PORT` | Port to run the server on | +| `FORGEJO_URL` | URL to your Forgejo instance | +| `FORGEJO_TOKEN` | Personal access token with repo read access | +| `BRANCH` | Branch to serve files from (e.g., `static-pages`) | +| `REPO` | Repository name to use (defaults to `pages`) | + +## How It Works + +Requesting `/username/path/to/file` will fetch: + +``` +https://FORGEJO_URL/api/v1/repos/username/pages/raw/path/to/file?ref=BRANCH +``` + +If a request ends in `/`, it automatically appends `index.html`. + +## License + +[MIT](LICENSE) diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..d39c4b9 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": true + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineEnding": "lf" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "indentStyle": "tab", + "lineEnding": "lf", + "jsxQuoteStyle": "double", + "semicolons": "always" + } + } +} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..7920daa --- /dev/null +++ b/compose.yml @@ -0,0 +1,16 @@ +services: + profile-page: + container_name: forgejoPages + build: + context: . + restart: unless-stopped + ports: + - "${PORT:-6600}:${PORT:-6600}" + env_file: + - .env + networks: + - forgejoPages-network + +networks: + forgejoPages-network: + driver: bridge diff --git a/config/environment.ts b/config/environment.ts new file mode 100644 index 0000000..5c9ef35 --- /dev/null +++ b/config/environment.ts @@ -0,0 +1,44 @@ +import { logger } from "@creations.works/logger"; + +const environment: Environment = { + port: Number.parseInt(process.env.PORT || "8080", 10), + host: process.env.HOST || "0.0.0.0", + development: + process.env.NODE_ENV === "development" || process.argv.includes("--dev"), +}; + +const forgejo = { + url: process.env.FORGEJO_URL || "", + token: process.env.FORGEJO_TOKEN || "", + branch: process.env.BRANCH || "static-pages", + repo: process.env.REPO || "pages", +}; + +function verifyRequiredVariables(): void { + const requiredVariables = [ + "HOST", + "PORT", + + "FORGEJO_URL", + "FORGEJO_TOKEN", + + "BRANCH", + "REPO", + ]; + + let hasError = false; + + for (const key of requiredVariables) { + const value = process.env[key]; + if (value === undefined || value.trim() === "") { + logger.error(`Missing or empty environment variable: ${key}`); + hasError = true; + } + } + + if (hasError) { + process.exit(1); + } +} + +export { environment, forgejo, verifyRequiredVariables }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c8319d3 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "forgejo_pages", + "module": "src/index.ts", + "type": "module", + "scripts": { + "start": "bun run src/index.ts", + "dev": "bun run --hot src/index.ts --dev", + "lint": "bunx biome check", + "lint:fix": "bunx biome check --fix", + "cleanup": "rm -rf logs node_modules bun.lockdb" + }, + "devDependencies": { + "@types/bun": "^1.2.13", + "globals": "^16.1.0", + "@biomejs/biome": "^1.9.4" + }, + "peerDependencies": { + "typescript": "^5.8.3" + }, + "dependencies": { + "@creations.works/logger": "^1.0.3", + "mime": "^4.0.7" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..aa62357 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,76 @@ +import { + environment, + forgejo, + verifyRequiredVariables, +} from "@config/environment"; +import { logger } from "@creations.works/logger"; +import { fetch, serve } from "bun"; +import mime from "mime"; + +async function main(): Promise { + verifyRequiredVariables(); + + serve({ + port: environment.port, + hostname: environment.host, + async fetch(req) { + const url = new URL(req.url); + const parts = url.pathname.split("/").filter(Boolean); + + if (parts.length < 1) { + return new Response("Not found", { status: 404 }); + } + + // Redirect /username → /username/ to make relative paths work -_- + if ( + parts.length === 1 && + !url.pathname.endsWith("/") && + !url.pathname.includes(".") + ) { + return Response.redirect(`${url.pathname}/`, 301); + } + + const username = parts[0]; + + if (!username.match(/^[a-z0-9_-]+$/i)) { + return new Response("Not found", { status: 404 }); + } + + let filePath = parts.slice(1).join("/"); + + if (url.pathname.endsWith("/") || filePath === "") { + filePath += "index.html"; + } + + const apiUrl = `${forgejo.url}/api/v1/repos/${username}/${forgejo.repo}/raw/${filePath}?ref=${forgejo.branch}`; + logger.info(`Proxying: ${url.pathname} → ${apiUrl}`); + + const res = await fetch(apiUrl, { + headers: { + Authorization: `token ${forgejo.token}`, + }, + }); + + if (!res.ok) { + return new Response("File not found", { status: res.status }); + } + + const contentType = mime.getType(filePath) || "application/octet-stream"; + return new Response(res.body, { + status: res.status, + headers: { + "Content-Type": contentType, + }, + }); + }, + }); + + logger.info( + `Server running at http://${environment.host}:${environment.port}`, + ); +} + +main().catch((error: Error) => { + logger.error(["Error initializing the server:", error]); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..68a5a97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "@config/*": ["config/*"], + "@types/*": ["types/*"], + "@helpers/*": ["src/helpers/*"] + }, + "typeRoots": ["./src/types", "./node_modules/@types"], + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src", "types", "config"] +} diff --git a/types/config.d.ts b/types/config.d.ts new file mode 100644 index 0000000..57584ed --- /dev/null +++ b/types/config.d.ts @@ -0,0 +1,5 @@ +type Environment = { + port: number; + host: string; + development: boolean; +};