commit 9389fd5f8c0e48aaa1d752f346a69711ecdf803b Author: creations Date: Sun May 11 14:07:03 2025 -0400 first commit 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; +};