From f31790f9b68137eb9055467546d3b83997d52001 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 15:11:00 +0100 Subject: [PATCH 01/10] feat(backend): embed Vite UI with -tags embed - Add web package (go:embed dist, stub without tag) and DistRoot helper - mountFrontend: prefer embedded FS, else frontend/dist on disk - Container and CI build with embed; Makefile embed targets - Document embedded vs disk UI in README, MkDocs, CONTRIBUTING --- .github/workflows/ci.yml | 29 +++++++++++- CONTRIBUTING.md | 3 +- Containerfile | 4 +- Makefile | 13 +++++- README.md | 11 ++++- backend/frontend_mount.go | 69 ++++++++++++++++++++++++++++ backend/main.go | 23 +--------- backend/web/.gitignore | 1 + backend/web/embed.go | 8 ++++ backend/web/embed_stub.go | 8 ++++ backend/web/fs.go | 17 +++++++ docs/getting-started/index.md | 10 ++-- docs/getting-started/installation.md | 8 +++- docs/getting-started/production.md | 22 ++++++--- 14 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 backend/frontend_mount.go create mode 100644 backend/web/.gitignore create mode 100644 backend/web/embed.go create mode 100644 backend/web/embed_stub.go create mode 100644 backend/web/fs.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4b89bd..d76590e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,10 +113,35 @@ jobs: name: frontend-dist path: frontend/dist + backend-embed: + name: Backend (embedded UI) + runs-on: ubuntu-latest + needs: [frontend] + steps: + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: '1.25' + cache-dependency-path: backend/go.sum + + - name: Download frontend dist + uses: actions/download-artifact@v4 + with: + name: frontend-dist + path: frontend/dist + + - name: Build with embedded frontend + run: | + rm -rf backend/web/dist + cp -R frontend/dist backend/web/dist + cd backend && go build -tags embed -o filetree . + docker-build: name: Build runs-on: ubuntu-latest - needs: [commitlint, backend, gosec, frontend] + needs: [commitlint, backend, gosec, frontend, backend-embed] steps: - uses: actions/checkout@v6 @@ -197,7 +222,7 @@ jobs: ci-success: name: CI success runs-on: ubuntu-latest - needs: [commitlint, backend, gosec, frontend, docker-build] + needs: [commitlint, backend, gosec, frontend, backend-embed, docker-build] steps: - name: All checks passed run: echo "All checks passed" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1849789..a6e268c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,8 @@ Validate locally: `make commitlint` or `npx commitlint --last --verbose` 1. Create a branch from `main`. 2. Ensure `go build ./... && go vet ./... && go test ./...` passes in `backend`. 3. Ensure `npm run build` passes in `frontend`. -4. Open a PR with a clear description of the change. +4. Optional: `make build-backend-embed` (after `make build-frontend`) verifies `go build -tags embed` with the Vite output in `backend/web/dist`. +5. Open a PR with a clear description of the change. ## Code style diff --git a/Containerfile b/Containerfile index 5f89fd5..7d75c31 100644 --- a/Containerfile +++ b/Containerfile @@ -9,14 +9,14 @@ WORKDIR /app COPY backend/go.mod backend/go.sum ./ RUN go mod download COPY backend/ ./ -RUN CGO_ENABLED=0 go build -o filetree . +COPY --from=frontend /app/frontend/dist ./web/dist +RUN CGO_ENABLED=0 go build -tags embed -o filetree . FROM alpine:3.20 RUN apk add --no-cache ca-certificates WORKDIR /app COPY --from=backend /app/filetree . -COPY --from=frontend /app/frontend/dist ./frontend/dist EXPOSE 8080 diff --git a/Makefile b/Makefile index 183dfd3..5dd5568 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-backend build-frontend test commitlint clean run-backend run-frontend +.PHONY: build build-backend build-frontend build-backend-embed embed-frontend test commitlint clean run-backend run-frontend # Build Go binary (output: backend/filetree or backend/filetree.exe) build-backend: @@ -8,6 +8,15 @@ build-backend: build-frontend: cd frontend && npm run build +# Copy Vite output into backend/web/dist (required before build-backend-embed) +embed-frontend: + rm -rf backend/web/dist + cp -R frontend/dist backend/web/dist + +# Single binary with embedded UI (needs frontend/dist; use after build-frontend) +build-backend-embed: embed-frontend + cd backend && go build -tags embed -o filetree . + # Build both backend binary and frontend assets all: build-backend build-frontend @@ -34,4 +43,4 @@ run-frontend: # Remove build artifacts clean: rm -f backend/filetree backend/filetree.exe - rm -rf frontend/dist + rm -rf frontend/dist backend/web/dist diff --git a/README.md b/README.md index e4c2698..7cb8e0f 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,20 @@ If the package is private, run `docker login ghcr.io` before `docker pull`. ### Production binary +**Embedded UI (single artifact)** — build the frontend, copy its output into `backend/web/dist`, then compile with the `embed` tag: + ```bash git clone https://github.com/heapoftrash/filetree.git -make build +make build-frontend +make build-backend-embed ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./backend/filetree ``` -Ship `backend/filetree` and `frontend/dist`. The binary serves the built UI when `frontend/dist` is present. +Or run `make embed-frontend` after `make build-frontend`; both populate `backend/web/dist` before `go build -tags embed`. + +The **prebuilt container image** already uses an embedded UI (no separate `frontend/dist` in the image). + +**UI on disk (optional)** — `make build` and run `./backend/filetree` from the repo root; the binary still serves `frontend/dist` when that folder exists next to the working directory. See [docs/API.md](docs/API.md) for the API. Build docs locally: `pip install mkdocs-material && mkdocs serve`. diff --git a/backend/frontend_mount.go b/backend/frontend_mount.go new file mode 100644 index 0000000..b6df049 --- /dev/null +++ b/backend/frontend_mount.go @@ -0,0 +1,69 @@ +package main + +import ( + "io/fs" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/heapoftrash/filetree/backend/web" +) + +func mountFrontend(r *gin.Engine) { + if mountEmbeddedFrontend(r) { + return + } + mountDiskFrontend(r) +} + +func mountEmbeddedFrontend(r *gin.Engine) bool { + distFS, ok := web.DistRoot() + if !ok { + return false + } + httpDist := http.FS(distFS) + if assetsFS, err := fs.Sub(distFS, "assets"); err == nil { + r.StaticFS("/assets", http.FS(assetsFS)) + } + for _, name := range []string{"favicon.svg", "icon-light.svg"} { + n := name + if _, err := fs.Stat(distFS, n); err != nil { + continue + } + r.GET("/"+n, func(c *gin.Context) { + c.FileFromFS(n, httpDist) + }) + } + r.StaticFileFS("/", "index.html", httpDist) + r.NoRoute(func(c *gin.Context) { + c.FileFromFS("index.html", httpDist) + }) + log.Println("Serving embedded frontend (built with -tags embed)") + return true +} + +func mountDiskFrontend(r *gin.Engine) { + var frontendDir string + for _, p := range []string{"./frontend/dist", "../frontend/dist"} { + if _, err := os.Stat(p + "/index.html"); err == nil { + frontendDir = p + break + } + } + if frontendDir == "" { + return + } + r.Static("/assets", frontendDir+"/assets") + if _, err := os.Stat(frontendDir + "/favicon.svg"); err == nil { + r.StaticFile("/favicon.svg", frontendDir+"/favicon.svg") + } + if _, err := os.Stat(frontendDir + "/icon-light.svg"); err == nil { + r.StaticFile("/icon-light.svg", frontendDir+"/icon-light.svg") + } + r.StaticFile("/", frontendDir+"/index.html") + r.NoRoute(func(c *gin.Context) { + c.File(frontendDir + "/index.html") + }) + log.Printf("Serving frontend from disk (%s)\n", frontendDir) +} diff --git a/backend/main.go b/backend/main.go index 4723768..2c8ae3d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -83,27 +83,8 @@ func main() { } } - // Serve frontend in production when built (try cwd and parent for frontend/dist) - var frontendDir string - for _, p := range []string{"./frontend/dist", "../frontend/dist"} { - if _, err := os.Stat(p + "/index.html"); err == nil { - frontendDir = p - break - } - } - if frontendDir != "" { - r.Static("/assets", frontendDir+"/assets") - if _, err := os.Stat(frontendDir + "/favicon.svg"); err == nil { - r.StaticFile("/favicon.svg", frontendDir+"/favicon.svg") - } - if _, err := os.Stat(frontendDir + "/icon-light.svg"); err == nil { - r.StaticFile("/icon-light.svg", frontendDir+"/icon-light.svg") - } - r.StaticFile("/", frontendDir+"/index.html") - r.NoRoute(func(c *gin.Context) { - c.File(frontendDir + "/index.html") - }) - } + // Embedded UI (-tags embed + web/dist) or ./frontend/dist, ../frontend/dist on disk + mountFrontend(r) log.Printf("filetree API listening on :8080 (ROOT_PATH=%s)", cfg.Server.RootPath) if err := r.Run(":8080"); err != nil { diff --git a/backend/web/.gitignore b/backend/web/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/backend/web/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/backend/web/embed.go b/backend/web/embed.go new file mode 100644 index 0000000..3d213b6 --- /dev/null +++ b/backend/web/embed.go @@ -0,0 +1,8 @@ +//go:build embed + +package web + +import "embed" + +//go:embed all:dist +var Dist embed.FS diff --git a/backend/web/embed_stub.go b/backend/web/embed_stub.go new file mode 100644 index 0000000..c190caf --- /dev/null +++ b/backend/web/embed_stub.go @@ -0,0 +1,8 @@ +//go:build !embed + +package web + +import "embed" + +// Dist is empty unless the binary is built with -tags embed and web/dist contains the Vite output. +var Dist embed.FS diff --git a/backend/web/fs.go b/backend/web/fs.go new file mode 100644 index 0000000..6b510a4 --- /dev/null +++ b/backend/web/fs.go @@ -0,0 +1,17 @@ +package web + +import ( + "io/fs" +) + +// DistRoot returns an fs.FS rooted at the embedded or copied dist/ tree when index.html is present. +func DistRoot() (fs.FS, bool) { + sub, err := fs.Sub(Dist, "dist") + if err != nil { + return nil, false + } + if _, err := fs.Stat(sub, "index.html"); err != nil { + return nil, false + } + return sub, true +} diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index d34a957..700a5e1 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -27,23 +27,23 @@ Over time, it grew, I added auth, previews, signed URLs, and an admin UI. What b ## Quick start -## Quick start - === "Single binary" - **Download** pre-built binaries from [GitHub Releases](https://github.com/heapoftrash/filetree/releases/latest). Extract the archive, copy `frontend/dist` next to the binary, then: + **Download** pre-built binaries from [GitHub Releases](https://github.com/heapoftrash/filetree/releases/latest) if available. If the release ships a UI bundle, place `frontend/dist` where the binary expects it (see [Installation](installation.md)), then: ```bash ROOT_PATH=./data ./filetree ``` - Or build from source: + Or build from source with the **embedded UI** (one file to run): ```bash - make build + make build-frontend && make build-backend-embed ROOT_PATH=./data ./backend/filetree ``` + Or `make build` and run `./backend/filetree` from the repo root so it serves `frontend/dist` from disk. + For production: ```bash diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 9dc1c87..d16a9b3 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -18,7 +18,7 @@ Filetree runs as a single binary that serves both the API and the frontend. Prer ROOT_PATH=./data ./backend/filetree ``` - Then open **http://localhost:8080**. The binary serves the frontend from `frontend/dist` when present. + Then open **http://localhost:8080**. By default the binary serves the UI from `frontend/dist` when that folder exists relative to the working directory. For a **single binary** with the UI inside it, run `make build-frontend` and `make build-backend-embed` instead of `build-backend`, then run `./backend/filetree` (built with `-tags embed`). For production, set `CONFIG_FILE` and `ROOT_PATH`: @@ -57,9 +57,13 @@ Filetree runs as a single binary that serves both the API and the frontend. Prer ```bash cd frontend && npm install && npm run build - cd ../backend && go mod tidy && go build -o filetree . + cd .. + rm -rf backend/web/dist && cp -R frontend/dist backend/web/dist + cd backend && go mod tidy && go build -tags embed -o filetree . ``` + Omit `-tags embed` and the `web/dist` copy if you prefer to run with `frontend/dist` on disk (e.g. run `./backend/filetree` from the repo root after only `go build -o filetree .`). + Run from the project root: ```bash diff --git a/docs/getting-started/production.md b/docs/getting-started/production.md index 101994b..e82d73d 100644 --- a/docs/getting-started/production.md +++ b/docs/getting-started/production.md @@ -17,11 +17,16 @@ Then open **http://localhost:8080**. ### Option A: Binary -1. **Copy** the `filetree` binary and the `frontend/dist` folder to your server -2. **Set** `ROOT_PATH` to the directory where files should be stored -3. **Set** `CONFIG_FILE` to your config file (e.g. `./config.yaml`) -4. **Use** a reverse proxy (nginx, Caddy) for the production URL if needed -5. **Set** `JWT_SECRET` and `oauth_redirect_url` for production (e.g. `https://your-domain.com/api/auth/google/callback`) +**Embedded UI (recommended for a single file to deploy):** from a source tree, run `make build-frontend` then `make build-backend-embed`. Deploy only `backend/filetree` (plus your config). The UI is baked into the binary via `go:embed` and `-tags embed`. + +**UI beside the binary:** if you build with `make build` (or `go build` without `embed`), copy **`frontend/dist`** next to the runtime layout the binary expects (`./frontend/dist` or `../frontend/dist` relative to the process working directory), or run from the repository root after `make build`. + +Then: + +1. **Set** `ROOT_PATH` to the directory where files should be stored +2. **Set** `CONFIG_FILE` to your config file (e.g. `./config.yaml`) +3. **Use** a reverse proxy (nginx, Caddy) for the production URL if needed +4. **Set** `JWT_SECRET` and `oauth_redirect_url` for production (e.g. `https://your-domain.com/api/auth/google/callback`) ### Option B: Docker @@ -30,6 +35,9 @@ Then open **http://localhost:8080**. 3. **Mount** your config file and set `CONFIG_FILE` if needed 4. **Use** a reverse proxy in front for HTTPS and custom domains -## Single binary +## Serving the UI + +- **Embedded build** (`-tags embed`, with `backend/web/dist` populated at compile time): the backend serves the UI from memory; no `frontend/dist` folder at runtime. +- **Disk layout:** when `./frontend/dist` or `../frontend/dist` exists (from the process working directory), the backend serves those files instead. -When `frontend/dist` exists next to the binary, the backend serves it directly. No separate web server is required for the frontend. For HTTPS and custom domains, put a reverse proxy in front. +No separate web server is required for the frontend. For HTTPS and custom domains, put a reverse proxy in front. From 7fbc1c5ccff95b36439c27f12430a5b0ee7c3253 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 15:21:58 +0100 Subject: [PATCH 02/10] refactor!: move server to app/ and Vite UI to app/web/ - Module path: github.com/heapoftrash/filetree/app - Colocate embed with Vite outDir (app/web/dist); drop cp step in Makefile/CI - Containerfile, dockerignore, dependabot, labeler, CI job ids (app, app-embed) - Update README, CONTRIBUTING, and getting-started docs --- .dockerignore | 6 ++- .github/dependabot.yml | 4 +- .github/labeler.yml | 20 +++++++-- .github/workflows/ci.yml | 45 +++++++++---------- .gitignore | 9 ++-- CONTRIBUTING.md | 16 +++---- Containerfile | 14 +++--- Makefile | 41 ++++++++--------- README.md | 20 ++++----- {backend => app}/config/config.go | 16 +++---- {backend => app}/config/registry.go | 0 {backend => app}/frontend_mount.go | 4 +- {backend => app}/go.mod | 2 +- {backend => app}/go.sum | 0 {backend => app}/handlers/auth.go | 38 ++++++++-------- {backend => app}/handlers/config.go | 2 +- {backend => app}/handlers/copy.go | 0 {backend => app}/handlers/create.go | 2 +- {backend => app}/handlers/download.go | 0 {backend => app}/handlers/handlers.go | 4 +- {backend => app}/handlers/list.go | 0 {backend => app}/handlers/move.go | 0 {backend => app}/handlers/preview.go | 0 {backend => app}/handlers/rename.go | 0 {backend => app}/handlers/signedurl.go | 2 +- {backend => app}/handlers/trash.go | 0 {backend => app}/handlers/zip.go | 0 {backend => app}/main.go | 8 ++-- {backend => app}/middleware/admin.go | 0 {backend => app}/middleware/auth.go | 0 {backend => app}/middleware/cors.go | 0 {backend => app}/middleware/signedurl.go | 0 {backend => app}/web/.gitignore | 0 {backend => app}/web/embed.go | 0 {backend => app}/web/embed_stub.go | 0 {backend => app}/web/fs.go | 0 {frontend => app/web}/index.html | 0 {frontend => app/web}/package-lock.json | 0 {frontend => app/web}/package.json | 0 {frontend => app/web}/public/favicon.svg | 0 {frontend => app/web}/public/icon-light.svg | 0 {frontend => app/web}/src/App.tsx | 0 {frontend => app/web}/src/api/client.ts | 0 .../web}/src/components/AppHeader.tsx | 0 .../components/FileManager/FileBreadcrumb.tsx | 0 .../src/components/FileManager/FileList.tsx | 0 .../components/FileManager/FileListGrid.tsx | 0 .../components/FileManager/FileListTable.tsx | 0 .../FileManager/FileListToolbar.tsx | 0 .../FileManager/FileManagerLayout.tsx | 0 .../FileManager/FileManagerMain.tsx | 0 .../FileManager/FileTreeSidebar.tsx | 0 .../FileManager/FullscreenPreview.tsx | 0 .../components/FileManager/PreviewContent.tsx | 0 .../components/FileManager/PreviewPanel.tsx | 0 .../web}/src/components/FileManager/hooks.ts | 0 .../web}/src/components/FileManager/index.ts | 0 .../FileManager/modals/ConflictModal.tsx | 0 .../FileManager/modals/MoveCopyModal.tsx | 0 .../FileManager/modals/NewFolderModal.tsx | 0 .../FileManager/modals/RenameModal.tsx | 0 .../components/FileManager/modals/index.ts | 0 .../FileManager/useFileManagerActions.tsx | 0 .../FileManager/useFileManagerConstants.ts | 0 .../FileManager/useFileManagerKeyboard.ts | 0 .../FileManager/useFileManagerState.tsx | 0 .../FileManager/useFileManagerUI.ts | 0 .../FileManager/usePreviewNavigation.ts | 0 .../web}/src/components/Login.tsx | 0 .../src/components/ProtectedAdminRoute.tsx | 0 .../web}/src/components/ProtectedRoute.tsx | 0 .../web}/src/components/Settings.tsx | 0 .../web}/src/components/StandalonePreview.tsx | 0 .../web}/src/contexts/AuthContext.tsx | 0 .../web}/src/contexts/ThemeContext.tsx | 0 {frontend => app/web}/src/index.css | 0 {frontend => app/web}/src/main.tsx | 0 .../web}/src/preview/AudioPreview.tsx | 0 .../web}/src/preview/CsvPreview.tsx | 0 .../web}/src/preview/HtmlPreview.tsx | 0 .../web}/src/preview/ImagePreview.tsx | 0 .../web}/src/preview/JsonPreview.tsx | 0 .../web}/src/preview/MarkdownPreview.tsx | 0 .../web}/src/preview/PdfPreview.tsx | 0 .../web}/src/preview/PreviewLoader.tsx | 0 .../web}/src/preview/TextPreview.tsx | 0 .../web}/src/preview/VideoPreview.tsx | 0 {frontend => app/web}/src/preview/config.ts | 0 {frontend => app/web}/src/preview/index.ts | 0 .../web}/src/preview/searchHighlight.tsx | 0 {frontend => app/web}/src/types/index.ts | 0 {frontend => app/web}/src/utils/errors.ts | 0 {frontend => app/web}/src/utils/fileUtils.tsx | 0 .../web}/src/utils/formatBytes.ts | 0 {frontend => app/web}/src/utils/pathUtils.ts | 0 {frontend => app/web}/src/vite-env.d.ts | 0 {frontend => app/web}/tsconfig.json | 0 {frontend => app/web}/tsconfig.node.json | 0 .../web}/tsconfig.node.tsbuildinfo | 0 {frontend => app/web}/tsconfig.tsbuildinfo | 0 {frontend => app/web}/vite.config.d.ts | 0 {frontend => app/web}/vite.config.js | 0 {frontend => app/web}/vite.config.ts | 0 docs/getting-started/index.md | 12 ++--- docs/getting-started/installation.md | 32 +++++++------ docs/getting-started/production.md | 12 ++--- 106 files changed, 158 insertions(+), 151 deletions(-) rename {backend => app}/config/config.go (94%) rename {backend => app}/config/registry.go (100%) rename {backend => app}/frontend_mount.go (93%) rename {backend => app}/go.mod (97%) rename {backend => app}/go.sum (100%) rename {backend => app}/handlers/auth.go (94%) rename {backend => app}/handlers/config.go (99%) rename {backend => app}/handlers/copy.go (100%) rename {backend => app}/handlers/create.go (99%) rename {backend => app}/handlers/download.go (100%) rename {backend => app}/handlers/handlers.go (96%) rename {backend => app}/handlers/list.go (100%) rename {backend => app}/handlers/move.go (100%) rename {backend => app}/handlers/preview.go (100%) rename {backend => app}/handlers/rename.go (100%) rename {backend => app}/handlers/signedurl.go (94%) rename {backend => app}/handlers/trash.go (100%) rename {backend => app}/handlers/zip.go (100%) rename {backend => app}/main.go (91%) rename {backend => app}/middleware/admin.go (100%) rename {backend => app}/middleware/auth.go (100%) rename {backend => app}/middleware/cors.go (100%) rename {backend => app}/middleware/signedurl.go (100%) rename {backend => app}/web/.gitignore (100%) rename {backend => app}/web/embed.go (100%) rename {backend => app}/web/embed_stub.go (100%) rename {backend => app}/web/fs.go (100%) rename {frontend => app/web}/index.html (100%) rename {frontend => app/web}/package-lock.json (100%) rename {frontend => app/web}/package.json (100%) rename {frontend => app/web}/public/favicon.svg (100%) rename {frontend => app/web}/public/icon-light.svg (100%) rename {frontend => app/web}/src/App.tsx (100%) rename {frontend => app/web}/src/api/client.ts (100%) rename {frontend => app/web}/src/components/AppHeader.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileBreadcrumb.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileList.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileListGrid.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileListTable.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileListToolbar.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileManagerLayout.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileManagerMain.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FileTreeSidebar.tsx (100%) rename {frontend => app/web}/src/components/FileManager/FullscreenPreview.tsx (100%) rename {frontend => app/web}/src/components/FileManager/PreviewContent.tsx (100%) rename {frontend => app/web}/src/components/FileManager/PreviewPanel.tsx (100%) rename {frontend => app/web}/src/components/FileManager/hooks.ts (100%) rename {frontend => app/web}/src/components/FileManager/index.ts (100%) rename {frontend => app/web}/src/components/FileManager/modals/ConflictModal.tsx (100%) rename {frontend => app/web}/src/components/FileManager/modals/MoveCopyModal.tsx (100%) rename {frontend => app/web}/src/components/FileManager/modals/NewFolderModal.tsx (100%) rename {frontend => app/web}/src/components/FileManager/modals/RenameModal.tsx (100%) rename {frontend => app/web}/src/components/FileManager/modals/index.ts (100%) rename {frontend => app/web}/src/components/FileManager/useFileManagerActions.tsx (100%) rename {frontend => app/web}/src/components/FileManager/useFileManagerConstants.ts (100%) rename {frontend => app/web}/src/components/FileManager/useFileManagerKeyboard.ts (100%) rename {frontend => app/web}/src/components/FileManager/useFileManagerState.tsx (100%) rename {frontend => app/web}/src/components/FileManager/useFileManagerUI.ts (100%) rename {frontend => app/web}/src/components/FileManager/usePreviewNavigation.ts (100%) rename {frontend => app/web}/src/components/Login.tsx (100%) rename {frontend => app/web}/src/components/ProtectedAdminRoute.tsx (100%) rename {frontend => app/web}/src/components/ProtectedRoute.tsx (100%) rename {frontend => app/web}/src/components/Settings.tsx (100%) rename {frontend => app/web}/src/components/StandalonePreview.tsx (100%) rename {frontend => app/web}/src/contexts/AuthContext.tsx (100%) rename {frontend => app/web}/src/contexts/ThemeContext.tsx (100%) rename {frontend => app/web}/src/index.css (100%) rename {frontend => app/web}/src/main.tsx (100%) rename {frontend => app/web}/src/preview/AudioPreview.tsx (100%) rename {frontend => app/web}/src/preview/CsvPreview.tsx (100%) rename {frontend => app/web}/src/preview/HtmlPreview.tsx (100%) rename {frontend => app/web}/src/preview/ImagePreview.tsx (100%) rename {frontend => app/web}/src/preview/JsonPreview.tsx (100%) rename {frontend => app/web}/src/preview/MarkdownPreview.tsx (100%) rename {frontend => app/web}/src/preview/PdfPreview.tsx (100%) rename {frontend => app/web}/src/preview/PreviewLoader.tsx (100%) rename {frontend => app/web}/src/preview/TextPreview.tsx (100%) rename {frontend => app/web}/src/preview/VideoPreview.tsx (100%) rename {frontend => app/web}/src/preview/config.ts (100%) rename {frontend => app/web}/src/preview/index.ts (100%) rename {frontend => app/web}/src/preview/searchHighlight.tsx (100%) rename {frontend => app/web}/src/types/index.ts (100%) rename {frontend => app/web}/src/utils/errors.ts (100%) rename {frontend => app/web}/src/utils/fileUtils.tsx (100%) rename {frontend => app/web}/src/utils/formatBytes.ts (100%) rename {frontend => app/web}/src/utils/pathUtils.ts (100%) rename {frontend => app/web}/src/vite-env.d.ts (100%) rename {frontend => app/web}/tsconfig.json (100%) rename {frontend => app/web}/tsconfig.node.json (100%) rename {frontend => app/web}/tsconfig.node.tsbuildinfo (100%) rename {frontend => app/web}/tsconfig.tsbuildinfo (100%) rename {frontend => app/web}/vite.config.d.ts (100%) rename {frontend => app/web}/vite.config.js (100%) rename {frontend => app/web}/vite.config.ts (100%) diff --git a/.dockerignore b/.dockerignore index 44cd24c..b1c5634 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,10 @@ .github docs node_modules -backend/filetree -backend/filetree.exe +app/web/node_modules +app/web/dist +app/filetree +app/filetree.exe *.md .env .env.* diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 90a75ed..82d0cb0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: - package-ecosystem: "gomod" - directory: "/backend" + directory: "/app" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -13,7 +13,7 @@ updates: open-pull-requests-limit: 5 - package-ecosystem: "npm" - directory: "/frontend" + directory: "/app/web" schedule: interval: "weekly" open-pull-requests-limit: 5 diff --git a/.github/labeler.yml b/.github/labeler.yml index 9ea96cf..75ae9a1 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,9 +4,23 @@ Documentation: Backend: - changed-files: - - any-glob-to-any-file: backend/** + - any-glob-to-any-file: app/config/** + - any-glob-to-any-file: app/handlers/** + - any-glob-to-any-file: app/middleware/** + - any-glob-to-any-file: app/main.go + - any-glob-to-any-file: app/frontend_mount.go + - any-glob-to-any-file: app/go.mod + - any-glob-to-any-file: app/go.sum + - any-glob-to-any-file: app/web/embed.go + - any-glob-to-any-file: app/web/embed_stub.go + - any-glob-to-any-file: app/web/fs.go Frontend: - changed-files: - - any-glob-to-any-file: frontend/** - + - any-glob-to-any-file: app/web/src/** + - any-glob-to-any-file: app/web/public/** + - any-glob-to-any-file: app/web/index.html + - any-glob-to-any-file: app/web/package.json + - any-glob-to-any-file: app/web/package-lock.json + - any-glob-to-any-file: app/web/vite.config.* + - any-glob-to-any-file: app/web/tsconfig*.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d76590e..bb7a10a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,8 @@ jobs: if: github.event_name == 'pull_request' run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose - backend: - name: Backend + app: + name: App (Go) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -54,22 +54,22 @@ jobs: uses: actions/setup-go@v6 with: go-version: '1.25' - cache-dependency-path: backend/go.sum + cache-dependency-path: app/go.sum - name: Build - run: cd backend && go build -o filetree . + run: cd app && go build -o filetree . - name: Vet - run: cd backend && go vet ./... + run: cd app && go vet ./... - name: Test - run: cd backend && go test ./... + run: cd app && go test ./... - name: Lint uses: golangci/golangci-lint-action@v9 with: version: v2.10 - working-directory: backend + working-directory: app gosec: name: Go security scan @@ -81,12 +81,12 @@ jobs: uses: actions/setup-go@v6 with: go-version: '1.25' - cache-dependency-path: backend/go.sum + cache-dependency-path: app/go.sum - name: Run gosec run: | go install github.com/securego/gosec/v2/cmd/gosec@latest - cd backend && gosec ./... + cd app && gosec ./... frontend: name: Frontend @@ -99,22 +99,22 @@ jobs: with: node-version: '20' cache: 'npm' - cache-dependency-path: frontend/package-lock.json + cache-dependency-path: app/web/package-lock.json - name: Install dependencies - run: cd frontend && npm ci + run: cd app/web && npm ci - name: Build - run: cd frontend && npm run build + run: cd app/web && npm run build - name: Upload frontend artifact uses: actions/upload-artifact@v4 with: name: frontend-dist - path: frontend/dist + path: app/web/dist - backend-embed: - name: Backend (embedded UI) + app-embed: + name: App (embedded UI) runs-on: ubuntu-latest needs: [frontend] steps: @@ -124,24 +124,21 @@ jobs: uses: actions/setup-go@v6 with: go-version: '1.25' - cache-dependency-path: backend/go.sum + cache-dependency-path: app/go.sum - name: Download frontend dist uses: actions/download-artifact@v4 with: name: frontend-dist - path: frontend/dist + path: app/web/dist - name: Build with embedded frontend - run: | - rm -rf backend/web/dist - cp -R frontend/dist backend/web/dist - cd backend && go build -tags embed -o filetree . + run: cd app && go build -tags embed -o filetree . docker-build: name: Build runs-on: ubuntu-latest - needs: [commitlint, backend, gosec, frontend, backend-embed] + needs: [commitlint, app, gosec, frontend, app-embed] steps: - uses: actions/checkout@v6 @@ -174,7 +171,7 @@ jobs: uses: actions/download-artifact@v4 with: name: frontend-dist - path: frontend/dist + path: app/web/dist - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -222,7 +219,7 @@ jobs: ci-success: name: CI success runs-on: ubuntu-latest - needs: [commitlint, backend, gosec, frontend, backend-embed, docker-build] + needs: [commitlint, app, gosec, frontend, app-embed, docker-build] steps: - name: All checks passed run: echo "All checks passed" diff --git a/.gitignore b/.gitignore index b3c74cc..3230e05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ # Go -backend/filetree -backend/filetree.exe -backend/data/ +app/filetree +app/filetree.exe +app/data/ -# Node / frontend +# Node / UI (also legacy paths) node_modules/ +app/web/dist/ frontend/dist/ # MkDocs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6e268c..ace8d37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,8 @@ Thank you for your interest in contributing. ## Development setup -1. **Backend:** `cd backend && go mod tidy && go run .` -2. **Frontend:** `cd frontend && npm install && npm run dev` +1. **Go (API):** `cd app && go mod tidy && go run .` +2. **UI:** `cd app/web && npm install && npm run dev` 3. Set `CONFIG_FILE=./config.yaml` and configure auth (see `config.example.yaml`). ## Commit messages @@ -21,9 +21,9 @@ Validate locally: `make commitlint` or `npx commitlint --last --verbose` ## Pull requests 1. Create a branch from `main`. -2. Ensure `go build ./... && go vet ./... && go test ./...` passes in `backend`. -3. Ensure `npm run build` passes in `frontend`. -4. Optional: `make build-backend-embed` (after `make build-frontend`) verifies `go build -tags embed` with the Vite output in `backend/web/dist`. +2. Ensure `go build ./... && go vet ./... && go test ./...` passes in `app`. +3. Ensure `npm run build` passes in `app/web`. +4. Optional: `make build-app-embed` verifies `go build -tags embed` after `make build-frontend`. 5. Open a PR with a clear description of the change. ## Code style @@ -33,7 +33,7 @@ Validate locally: `make commitlint` or `npx commitlint --last --verbose` ## Go score & quality -CI runs these checks on the backend: +CI runs these checks on the Go module under `app/`: | Tool | Purpose | |------|---------| @@ -46,10 +46,10 @@ CI runs these checks on the backend: Run locally: ```bash -cd backend +cd app go build ./... && go vet ./... && go test ./... go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && golangci-lint run go install github.com/securego/gosec/v2/cmd/gosec@latest && gosec ./... ``` -If you fork the repo, update the Go Report Card badge in the README to point to your fork (e.g. `github.com/YOUR_ORG/filetree`) to see the score at [goreportcard.com](https://goreportcard.com). +If you fork the repo, update the Go Report Card badge in the README to point to your fork module path (e.g. `github.com/YOUR_ORG/filetree/app`) to see the score at [goreportcard.com](https://goreportcard.com). diff --git a/Containerfile b/Containerfile index 7d75c31..965e8e5 100644 --- a/Containerfile +++ b/Containerfile @@ -1,15 +1,17 @@ FROM node:20-alpine AS frontend -WORKDIR /app/frontend -COPY frontend/ ./ +WORKDIR /app/web +COPY app/web/package.json app/web/package-lock.json ./ ENV NODE_OPTIONS="--max-old-space-size=4096" -RUN if [ ! -f dist/index.html ]; then npm ci && npm run build; fi +RUN npm ci +COPY app/web/ ./ +RUN npm run build FROM golang:1.25-alpine AS backend WORKDIR /app -COPY backend/go.mod backend/go.sum ./ +COPY app/go.mod app/go.sum ./ RUN go mod download -COPY backend/ ./ -COPY --from=frontend /app/frontend/dist ./web/dist +COPY app/ ./ +COPY --from=frontend /app/web/dist ./web/dist RUN CGO_ENABLED=0 go build -tags embed -o filetree . FROM alpine:3.20 diff --git a/Makefile b/Makefile index 5dd5568..c3d93b5 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,19 @@ -.PHONY: build build-backend build-frontend build-backend-embed embed-frontend test commitlint clean run-backend run-frontend +.PHONY: build build-app build-frontend build-app-embed test commitlint clean run run-frontend -# Build Go binary (output: backend/filetree or backend/filetree.exe) -build-backend: - cd backend && go build -o filetree . +# Build Go binary (output: app/filetree or app/filetree.exe) +build-app: + cd app && go build -o filetree . -# Build frontend (output: frontend/dist/) +# Build frontend (output: app/web/dist/) build-frontend: - cd frontend && npm run build + cd app/web && npm run build -# Copy Vite output into backend/web/dist (required before build-backend-embed) -embed-frontend: - rm -rf backend/web/dist - cp -R frontend/dist backend/web/dist +# Single binary with embedded UI (Vite writes to app/web/dist; no copy step) +build-app-embed: build-frontend + cd app && go build -tags embed -o filetree . -# Single binary with embedded UI (needs frontend/dist; use after build-frontend) -build-backend-embed: embed-frontend - cd backend && go build -tags embed -o filetree . - -# Build both backend binary and frontend assets -all: build-backend build-frontend +# Build both Go binary and frontend assets +all: build-app build-frontend # Alias build: all @@ -29,18 +24,18 @@ commitlint: # Run tests test: - cd backend && go vet ./... && go test ./... - cd frontend && npm run build + cd app && go vet ./... && go test ./... + cd app/web && npm run build -# Run Go backend (dev) +# Run Go server (dev) run: - ./backend/filetree + ./app/filetree # Run frontend dev server (proxies /api to backend) run-frontend: - cd frontend && npm run dev + cd app/web && npm run dev # Remove build artifacts clean: - rm -f backend/filetree backend/filetree.exe - rm -rf frontend/dist backend/web/dist + rm -f app/filetree app/filetree.exe + rm -rf app/web/dist frontend/dist diff --git a/README.md b/README.md index 7cb8e0f..29e7823 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@

- - Go Report Card + + Go Report Card Build status @@ -70,28 +70,26 @@ If the package is private, run `docker login ghcr.io` before `docker pull`. ### Production binary -**Embedded UI (single artifact)** — build the frontend, copy its output into `backend/web/dist`, then compile with the `embed` tag: +**Embedded UI (single artifact)** — the Vite app lives in `app/web/` and writes to `app/web/dist`; build the frontend, then compile the Go app with the `embed` tag: ```bash git clone https://github.com/heapoftrash/filetree.git make build-frontend -make build-backend-embed -ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./backend/filetree +make build-app-embed +ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./app/filetree ``` -Or run `make embed-frontend` after `make build-frontend`; both populate `backend/web/dist` before `go build -tags embed`. +The **prebuilt container image** already uses an embedded UI (no separate static bundle directory in the image). -The **prebuilt container image** already uses an embedded UI (no separate `frontend/dist` in the image). - -**UI on disk (optional)** — `make build` and run `./backend/filetree` from the repo root; the binary still serves `frontend/dist` when that folder exists next to the working directory. +**UI on disk (optional)** — `make build` and run `./app/filetree` from the repo root; the binary still serves files from `app/web/dist` or `./web/dist` when you run it from the `app/` directory (see [documentation](https://heapoftrash.github.io/filetree/)). See [docs/API.md](docs/API.md) for the API. Build docs locally: `pip install mkdocs-material && mkdocs serve`. ### Development -**API** — `cd backend && go mod tidy && go run .` → **http://localhost:8080** +**API** — `cd app && go mod tidy && go run .` → **http://localhost:8080** -**UI** — `cd frontend && npm install && npm run dev` → **http://localhost:5173** (proxies `/api` to the backend) +**UI** — `cd app/web && npm install && npm run dev` → **http://localhost:5173** (proxies `/api` to the API) Set `frontend.url` in config to `http://localhost:5173` for CORS and OAuth. diff --git a/backend/config/config.go b/app/config/config.go similarity index 94% rename from backend/config/config.go rename to app/config/config.go index f847841..092e7bf 100644 --- a/backend/config/config.go +++ b/app/config/config.go @@ -41,19 +41,19 @@ type Config struct { Auth AuthConfig `yaml:"auth" json:"auth"` Frontend FrontendConfig `yaml:"frontend" json:"frontend"` Users UsersConfig `yaml:"users" json:"users"` - ConfigPath string `yaml:"-" json:"-"` // path we loaded from, for write-back + ConfigPath string `yaml:"-" json:"-"` // path we loaded from, for write-back } type ServerConfig struct { RootPath string `yaml:"root_path" json:"root_path"` - Debug bool `yaml:"debug" json:"debug"` // when false, Gin runs in release mode (no debug logs) - MaxUploadBytes int64 `yaml:"max_upload_bytes" json:"max_upload_bytes"` // max multipart upload size; 0 = default 100MB + Debug bool `yaml:"debug" json:"debug"` // when false, Gin runs in release mode (no debug logs) + MaxUploadBytes int64 `yaml:"max_upload_bytes" json:"max_upload_bytes"` // max multipart upload size; 0 = default 100MB } type AuthConfig struct { - JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` - OAuthRedirectURL string `yaml:"oauth_redirect_url" json:"oauth_redirect_url"` - LocalAuthEnabled bool `yaml:"local_auth_enabled" json:"local_auth_enabled"` // username/password auth + JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` + OAuthRedirectURL string `yaml:"oauth_redirect_url" json:"oauth_redirect_url"` + LocalAuthEnabled bool `yaml:"local_auth_enabled" json:"local_auth_enabled"` // username/password auth Providers map[string]ProviderConfig `yaml:"providers" json:"providers"` } @@ -70,8 +70,8 @@ type FrontendConfig struct { } type UsersConfig struct { - AdminEmails []string `yaml:"admin_emails" json:"admin_emails"` - LocalUsers []LocalUser `yaml:"local_users" json:"local_users"` + AdminEmails []string `yaml:"admin_emails" json:"admin_emails"` + LocalUsers []LocalUser `yaml:"local_users" json:"local_users"` DefaultAdmin *DefaultAdminUser `yaml:"default_admin" json:"default_admin"` } diff --git a/backend/config/registry.go b/app/config/registry.go similarity index 100% rename from backend/config/registry.go rename to app/config/registry.go diff --git a/backend/frontend_mount.go b/app/frontend_mount.go similarity index 93% rename from backend/frontend_mount.go rename to app/frontend_mount.go index b6df049..2c1ed4b 100644 --- a/backend/frontend_mount.go +++ b/app/frontend_mount.go @@ -7,7 +7,7 @@ import ( "os" "github.com/gin-gonic/gin" - "github.com/heapoftrash/filetree/backend/web" + "github.com/heapoftrash/filetree/app/web" ) func mountFrontend(r *gin.Engine) { @@ -45,7 +45,7 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { func mountDiskFrontend(r *gin.Engine) { var frontendDir string - for _, p := range []string{"./frontend/dist", "../frontend/dist"} { + for _, p := range []string{"./web/dist", "./app/web/dist"} { if _, err := os.Stat(p + "/index.html"); err == nil { frontendDir = p break diff --git a/backend/go.mod b/app/go.mod similarity index 97% rename from backend/go.mod rename to app/go.mod index d21b506..0f19c62 100644 --- a/backend/go.mod +++ b/app/go.mod @@ -1,4 +1,4 @@ -module github.com/heapoftrash/filetree/backend +module github.com/heapoftrash/filetree/app go 1.25.0 diff --git a/backend/go.sum b/app/go.sum similarity index 100% rename from backend/go.sum rename to app/go.sum diff --git a/backend/handlers/auth.go b/app/handlers/auth.go similarity index 94% rename from backend/handlers/auth.go rename to app/handlers/auth.go index d520483..ff1bb20 100644 --- a/backend/handlers/auth.go +++ b/app/handlers/auth.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/heapoftrash/filetree/backend/config" + "github.com/heapoftrash/filetree/app/config" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -26,14 +26,14 @@ var ( ) type AuthHandler struct { - oauth2Configs map[string]*oauth2.Config // provider id -> config - jwtSecret []byte - frontendURL string - adminEmails []string - localUsers []config.LocalUser - defaultAdmin *config.DefaultAdminUser - localAuthEnabled bool - oauthProviders []LoginProviderInfo + oauth2Configs map[string]*oauth2.Config // provider id -> config + jwtSecret []byte + frontendURL string + adminEmails []string + localUsers []config.LocalUser + defaultAdmin *config.DefaultAdminUser + localAuthEnabled bool + oauthProviders []LoginProviderInfo } // oauthCallbackURL derives the callback URL for a provider from the base oauth_redirect_url. @@ -120,7 +120,7 @@ func buildOAuthProviders(cfg *config.Config) []LoginProviderInfo { // LoginOptionsResponse is the public auth config for the login page. type LoginOptionsResponse struct { - LocalAuthEnabled bool `json:"local_auth_enabled"` + LocalAuthEnabled bool `json:"local_auth_enabled"` Providers []LoginProviderInfo `json:"providers"` } @@ -187,11 +187,11 @@ func (h *AuthHandler) LocalLogin(c *gin.Context) { } log.Printf("[auth] local user logged in: username=%q", authUsername) claims := jwt.MapClaims{ - "sub": authUsername, - "email": authUsername, - "name": authUsername, - "exp": time.Now().Add(tokenExpiry).Unix(), - "iat": time.Now().Unix(), + "sub": authUsername, + "email": authUsername, + "name": authUsername, + "exp": time.Now().Add(tokenExpiry).Unix(), + "iat": time.Now().Unix(), } jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := jwtToken.SignedString(h.jwtSecret) @@ -338,10 +338,10 @@ func (h *AuthHandler) fetchGitHubUserInfo(ctx context.Context, cfg *oauth2.Confi defer func() { _ = resp.Body.Close() }() var user struct { - Login string `json:"login"` - Name string `json:"name"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` + Login string `json:"login"` + Name string `json:"name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` } if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { return "", "", "", err diff --git a/backend/handlers/config.go b/app/handlers/config.go similarity index 99% rename from backend/handlers/config.go rename to app/handlers/config.go index 41937e7..89c687d 100644 --- a/backend/handlers/config.go +++ b/app/handlers/config.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/heapoftrash/filetree/backend/config" + "github.com/heapoftrash/filetree/app/config" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" diff --git a/backend/handlers/copy.go b/app/handlers/copy.go similarity index 100% rename from backend/handlers/copy.go rename to app/handlers/copy.go diff --git a/backend/handlers/create.go b/app/handlers/create.go similarity index 99% rename from backend/handlers/create.go rename to app/handlers/create.go index 073e1cd..bd79cb5 100644 --- a/backend/handlers/create.go +++ b/app/handlers/create.go @@ -83,4 +83,4 @@ func (h *Handler) upload(c *gin.Context) { } } c.JSON(http.StatusCreated, gin.H{"uploaded": len(files)}) -} \ No newline at end of file +} diff --git a/backend/handlers/download.go b/app/handlers/download.go similarity index 100% rename from backend/handlers/download.go rename to app/handlers/download.go diff --git a/backend/handlers/handlers.go b/app/handlers/handlers.go similarity index 96% rename from backend/handlers/handlers.go rename to app/handlers/handlers.go index fc92321..9f827f4 100644 --- a/backend/handlers/handlers.go +++ b/app/handlers/handlers.go @@ -5,8 +5,8 @@ import ( ) type Handler struct { - root string - maxUploadBytes int64 + root string + maxUploadBytes int64 } func New(root string, maxUploadBytes int64) (*Handler, error) { diff --git a/backend/handlers/list.go b/app/handlers/list.go similarity index 100% rename from backend/handlers/list.go rename to app/handlers/list.go diff --git a/backend/handlers/move.go b/app/handlers/move.go similarity index 100% rename from backend/handlers/move.go rename to app/handlers/move.go diff --git a/backend/handlers/preview.go b/app/handlers/preview.go similarity index 100% rename from backend/handlers/preview.go rename to app/handlers/preview.go diff --git a/backend/handlers/rename.go b/app/handlers/rename.go similarity index 100% rename from backend/handlers/rename.go rename to app/handlers/rename.go diff --git a/backend/handlers/signedurl.go b/app/handlers/signedurl.go similarity index 94% rename from backend/handlers/signedurl.go rename to app/handlers/signedurl.go index 37956d3..5174f6e 100644 --- a/backend/handlers/signedurl.go +++ b/app/handlers/signedurl.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "github.com/heapoftrash/filetree/backend/middleware" + "github.com/heapoftrash/filetree/app/middleware" "github.com/gin-gonic/gin" ) diff --git a/backend/handlers/trash.go b/app/handlers/trash.go similarity index 100% rename from backend/handlers/trash.go rename to app/handlers/trash.go diff --git a/backend/handlers/zip.go b/app/handlers/zip.go similarity index 100% rename from backend/handlers/zip.go rename to app/handlers/zip.go diff --git a/backend/main.go b/app/main.go similarity index 91% rename from backend/main.go rename to app/main.go index 2c8ae3d..0b84533 100644 --- a/backend/main.go +++ b/app/main.go @@ -4,9 +4,9 @@ import ( "log" "os" - "github.com/heapoftrash/filetree/backend/config" - "github.com/heapoftrash/filetree/backend/handlers" - "github.com/heapoftrash/filetree/backend/middleware" + "github.com/heapoftrash/filetree/app/config" + "github.com/heapoftrash/filetree/app/handlers" + "github.com/heapoftrash/filetree/app/middleware" "github.com/gin-gonic/gin" ) @@ -83,7 +83,7 @@ func main() { } } - // Embedded UI (-tags embed + web/dist) or ./frontend/dist, ../frontend/dist on disk + // Embedded UI (-tags embed + app/web/dist) or ./web/dist, ./app/web/dist on disk mountFrontend(r) log.Printf("filetree API listening on :8080 (ROOT_PATH=%s)", cfg.Server.RootPath) diff --git a/backend/middleware/admin.go b/app/middleware/admin.go similarity index 100% rename from backend/middleware/admin.go rename to app/middleware/admin.go diff --git a/backend/middleware/auth.go b/app/middleware/auth.go similarity index 100% rename from backend/middleware/auth.go rename to app/middleware/auth.go diff --git a/backend/middleware/cors.go b/app/middleware/cors.go similarity index 100% rename from backend/middleware/cors.go rename to app/middleware/cors.go diff --git a/backend/middleware/signedurl.go b/app/middleware/signedurl.go similarity index 100% rename from backend/middleware/signedurl.go rename to app/middleware/signedurl.go diff --git a/backend/web/.gitignore b/app/web/.gitignore similarity index 100% rename from backend/web/.gitignore rename to app/web/.gitignore diff --git a/backend/web/embed.go b/app/web/embed.go similarity index 100% rename from backend/web/embed.go rename to app/web/embed.go diff --git a/backend/web/embed_stub.go b/app/web/embed_stub.go similarity index 100% rename from backend/web/embed_stub.go rename to app/web/embed_stub.go diff --git a/backend/web/fs.go b/app/web/fs.go similarity index 100% rename from backend/web/fs.go rename to app/web/fs.go diff --git a/frontend/index.html b/app/web/index.html similarity index 100% rename from frontend/index.html rename to app/web/index.html diff --git a/frontend/package-lock.json b/app/web/package-lock.json similarity index 100% rename from frontend/package-lock.json rename to app/web/package-lock.json diff --git a/frontend/package.json b/app/web/package.json similarity index 100% rename from frontend/package.json rename to app/web/package.json diff --git a/frontend/public/favicon.svg b/app/web/public/favicon.svg similarity index 100% rename from frontend/public/favicon.svg rename to app/web/public/favicon.svg diff --git a/frontend/public/icon-light.svg b/app/web/public/icon-light.svg similarity index 100% rename from frontend/public/icon-light.svg rename to app/web/public/icon-light.svg diff --git a/frontend/src/App.tsx b/app/web/src/App.tsx similarity index 100% rename from frontend/src/App.tsx rename to app/web/src/App.tsx diff --git a/frontend/src/api/client.ts b/app/web/src/api/client.ts similarity index 100% rename from frontend/src/api/client.ts rename to app/web/src/api/client.ts diff --git a/frontend/src/components/AppHeader.tsx b/app/web/src/components/AppHeader.tsx similarity index 100% rename from frontend/src/components/AppHeader.tsx rename to app/web/src/components/AppHeader.tsx diff --git a/frontend/src/components/FileManager/FileBreadcrumb.tsx b/app/web/src/components/FileManager/FileBreadcrumb.tsx similarity index 100% rename from frontend/src/components/FileManager/FileBreadcrumb.tsx rename to app/web/src/components/FileManager/FileBreadcrumb.tsx diff --git a/frontend/src/components/FileManager/FileList.tsx b/app/web/src/components/FileManager/FileList.tsx similarity index 100% rename from frontend/src/components/FileManager/FileList.tsx rename to app/web/src/components/FileManager/FileList.tsx diff --git a/frontend/src/components/FileManager/FileListGrid.tsx b/app/web/src/components/FileManager/FileListGrid.tsx similarity index 100% rename from frontend/src/components/FileManager/FileListGrid.tsx rename to app/web/src/components/FileManager/FileListGrid.tsx diff --git a/frontend/src/components/FileManager/FileListTable.tsx b/app/web/src/components/FileManager/FileListTable.tsx similarity index 100% rename from frontend/src/components/FileManager/FileListTable.tsx rename to app/web/src/components/FileManager/FileListTable.tsx diff --git a/frontend/src/components/FileManager/FileListToolbar.tsx b/app/web/src/components/FileManager/FileListToolbar.tsx similarity index 100% rename from frontend/src/components/FileManager/FileListToolbar.tsx rename to app/web/src/components/FileManager/FileListToolbar.tsx diff --git a/frontend/src/components/FileManager/FileManagerLayout.tsx b/app/web/src/components/FileManager/FileManagerLayout.tsx similarity index 100% rename from frontend/src/components/FileManager/FileManagerLayout.tsx rename to app/web/src/components/FileManager/FileManagerLayout.tsx diff --git a/frontend/src/components/FileManager/FileManagerMain.tsx b/app/web/src/components/FileManager/FileManagerMain.tsx similarity index 100% rename from frontend/src/components/FileManager/FileManagerMain.tsx rename to app/web/src/components/FileManager/FileManagerMain.tsx diff --git a/frontend/src/components/FileManager/FileTreeSidebar.tsx b/app/web/src/components/FileManager/FileTreeSidebar.tsx similarity index 100% rename from frontend/src/components/FileManager/FileTreeSidebar.tsx rename to app/web/src/components/FileManager/FileTreeSidebar.tsx diff --git a/frontend/src/components/FileManager/FullscreenPreview.tsx b/app/web/src/components/FileManager/FullscreenPreview.tsx similarity index 100% rename from frontend/src/components/FileManager/FullscreenPreview.tsx rename to app/web/src/components/FileManager/FullscreenPreview.tsx diff --git a/frontend/src/components/FileManager/PreviewContent.tsx b/app/web/src/components/FileManager/PreviewContent.tsx similarity index 100% rename from frontend/src/components/FileManager/PreviewContent.tsx rename to app/web/src/components/FileManager/PreviewContent.tsx diff --git a/frontend/src/components/FileManager/PreviewPanel.tsx b/app/web/src/components/FileManager/PreviewPanel.tsx similarity index 100% rename from frontend/src/components/FileManager/PreviewPanel.tsx rename to app/web/src/components/FileManager/PreviewPanel.tsx diff --git a/frontend/src/components/FileManager/hooks.ts b/app/web/src/components/FileManager/hooks.ts similarity index 100% rename from frontend/src/components/FileManager/hooks.ts rename to app/web/src/components/FileManager/hooks.ts diff --git a/frontend/src/components/FileManager/index.ts b/app/web/src/components/FileManager/index.ts similarity index 100% rename from frontend/src/components/FileManager/index.ts rename to app/web/src/components/FileManager/index.ts diff --git a/frontend/src/components/FileManager/modals/ConflictModal.tsx b/app/web/src/components/FileManager/modals/ConflictModal.tsx similarity index 100% rename from frontend/src/components/FileManager/modals/ConflictModal.tsx rename to app/web/src/components/FileManager/modals/ConflictModal.tsx diff --git a/frontend/src/components/FileManager/modals/MoveCopyModal.tsx b/app/web/src/components/FileManager/modals/MoveCopyModal.tsx similarity index 100% rename from frontend/src/components/FileManager/modals/MoveCopyModal.tsx rename to app/web/src/components/FileManager/modals/MoveCopyModal.tsx diff --git a/frontend/src/components/FileManager/modals/NewFolderModal.tsx b/app/web/src/components/FileManager/modals/NewFolderModal.tsx similarity index 100% rename from frontend/src/components/FileManager/modals/NewFolderModal.tsx rename to app/web/src/components/FileManager/modals/NewFolderModal.tsx diff --git a/frontend/src/components/FileManager/modals/RenameModal.tsx b/app/web/src/components/FileManager/modals/RenameModal.tsx similarity index 100% rename from frontend/src/components/FileManager/modals/RenameModal.tsx rename to app/web/src/components/FileManager/modals/RenameModal.tsx diff --git a/frontend/src/components/FileManager/modals/index.ts b/app/web/src/components/FileManager/modals/index.ts similarity index 100% rename from frontend/src/components/FileManager/modals/index.ts rename to app/web/src/components/FileManager/modals/index.ts diff --git a/frontend/src/components/FileManager/useFileManagerActions.tsx b/app/web/src/components/FileManager/useFileManagerActions.tsx similarity index 100% rename from frontend/src/components/FileManager/useFileManagerActions.tsx rename to app/web/src/components/FileManager/useFileManagerActions.tsx diff --git a/frontend/src/components/FileManager/useFileManagerConstants.ts b/app/web/src/components/FileManager/useFileManagerConstants.ts similarity index 100% rename from frontend/src/components/FileManager/useFileManagerConstants.ts rename to app/web/src/components/FileManager/useFileManagerConstants.ts diff --git a/frontend/src/components/FileManager/useFileManagerKeyboard.ts b/app/web/src/components/FileManager/useFileManagerKeyboard.ts similarity index 100% rename from frontend/src/components/FileManager/useFileManagerKeyboard.ts rename to app/web/src/components/FileManager/useFileManagerKeyboard.ts diff --git a/frontend/src/components/FileManager/useFileManagerState.tsx b/app/web/src/components/FileManager/useFileManagerState.tsx similarity index 100% rename from frontend/src/components/FileManager/useFileManagerState.tsx rename to app/web/src/components/FileManager/useFileManagerState.tsx diff --git a/frontend/src/components/FileManager/useFileManagerUI.ts b/app/web/src/components/FileManager/useFileManagerUI.ts similarity index 100% rename from frontend/src/components/FileManager/useFileManagerUI.ts rename to app/web/src/components/FileManager/useFileManagerUI.ts diff --git a/frontend/src/components/FileManager/usePreviewNavigation.ts b/app/web/src/components/FileManager/usePreviewNavigation.ts similarity index 100% rename from frontend/src/components/FileManager/usePreviewNavigation.ts rename to app/web/src/components/FileManager/usePreviewNavigation.ts diff --git a/frontend/src/components/Login.tsx b/app/web/src/components/Login.tsx similarity index 100% rename from frontend/src/components/Login.tsx rename to app/web/src/components/Login.tsx diff --git a/frontend/src/components/ProtectedAdminRoute.tsx b/app/web/src/components/ProtectedAdminRoute.tsx similarity index 100% rename from frontend/src/components/ProtectedAdminRoute.tsx rename to app/web/src/components/ProtectedAdminRoute.tsx diff --git a/frontend/src/components/ProtectedRoute.tsx b/app/web/src/components/ProtectedRoute.tsx similarity index 100% rename from frontend/src/components/ProtectedRoute.tsx rename to app/web/src/components/ProtectedRoute.tsx diff --git a/frontend/src/components/Settings.tsx b/app/web/src/components/Settings.tsx similarity index 100% rename from frontend/src/components/Settings.tsx rename to app/web/src/components/Settings.tsx diff --git a/frontend/src/components/StandalonePreview.tsx b/app/web/src/components/StandalonePreview.tsx similarity index 100% rename from frontend/src/components/StandalonePreview.tsx rename to app/web/src/components/StandalonePreview.tsx diff --git a/frontend/src/contexts/AuthContext.tsx b/app/web/src/contexts/AuthContext.tsx similarity index 100% rename from frontend/src/contexts/AuthContext.tsx rename to app/web/src/contexts/AuthContext.tsx diff --git a/frontend/src/contexts/ThemeContext.tsx b/app/web/src/contexts/ThemeContext.tsx similarity index 100% rename from frontend/src/contexts/ThemeContext.tsx rename to app/web/src/contexts/ThemeContext.tsx diff --git a/frontend/src/index.css b/app/web/src/index.css similarity index 100% rename from frontend/src/index.css rename to app/web/src/index.css diff --git a/frontend/src/main.tsx b/app/web/src/main.tsx similarity index 100% rename from frontend/src/main.tsx rename to app/web/src/main.tsx diff --git a/frontend/src/preview/AudioPreview.tsx b/app/web/src/preview/AudioPreview.tsx similarity index 100% rename from frontend/src/preview/AudioPreview.tsx rename to app/web/src/preview/AudioPreview.tsx diff --git a/frontend/src/preview/CsvPreview.tsx b/app/web/src/preview/CsvPreview.tsx similarity index 100% rename from frontend/src/preview/CsvPreview.tsx rename to app/web/src/preview/CsvPreview.tsx diff --git a/frontend/src/preview/HtmlPreview.tsx b/app/web/src/preview/HtmlPreview.tsx similarity index 100% rename from frontend/src/preview/HtmlPreview.tsx rename to app/web/src/preview/HtmlPreview.tsx diff --git a/frontend/src/preview/ImagePreview.tsx b/app/web/src/preview/ImagePreview.tsx similarity index 100% rename from frontend/src/preview/ImagePreview.tsx rename to app/web/src/preview/ImagePreview.tsx diff --git a/frontend/src/preview/JsonPreview.tsx b/app/web/src/preview/JsonPreview.tsx similarity index 100% rename from frontend/src/preview/JsonPreview.tsx rename to app/web/src/preview/JsonPreview.tsx diff --git a/frontend/src/preview/MarkdownPreview.tsx b/app/web/src/preview/MarkdownPreview.tsx similarity index 100% rename from frontend/src/preview/MarkdownPreview.tsx rename to app/web/src/preview/MarkdownPreview.tsx diff --git a/frontend/src/preview/PdfPreview.tsx b/app/web/src/preview/PdfPreview.tsx similarity index 100% rename from frontend/src/preview/PdfPreview.tsx rename to app/web/src/preview/PdfPreview.tsx diff --git a/frontend/src/preview/PreviewLoader.tsx b/app/web/src/preview/PreviewLoader.tsx similarity index 100% rename from frontend/src/preview/PreviewLoader.tsx rename to app/web/src/preview/PreviewLoader.tsx diff --git a/frontend/src/preview/TextPreview.tsx b/app/web/src/preview/TextPreview.tsx similarity index 100% rename from frontend/src/preview/TextPreview.tsx rename to app/web/src/preview/TextPreview.tsx diff --git a/frontend/src/preview/VideoPreview.tsx b/app/web/src/preview/VideoPreview.tsx similarity index 100% rename from frontend/src/preview/VideoPreview.tsx rename to app/web/src/preview/VideoPreview.tsx diff --git a/frontend/src/preview/config.ts b/app/web/src/preview/config.ts similarity index 100% rename from frontend/src/preview/config.ts rename to app/web/src/preview/config.ts diff --git a/frontend/src/preview/index.ts b/app/web/src/preview/index.ts similarity index 100% rename from frontend/src/preview/index.ts rename to app/web/src/preview/index.ts diff --git a/frontend/src/preview/searchHighlight.tsx b/app/web/src/preview/searchHighlight.tsx similarity index 100% rename from frontend/src/preview/searchHighlight.tsx rename to app/web/src/preview/searchHighlight.tsx diff --git a/frontend/src/types/index.ts b/app/web/src/types/index.ts similarity index 100% rename from frontend/src/types/index.ts rename to app/web/src/types/index.ts diff --git a/frontend/src/utils/errors.ts b/app/web/src/utils/errors.ts similarity index 100% rename from frontend/src/utils/errors.ts rename to app/web/src/utils/errors.ts diff --git a/frontend/src/utils/fileUtils.tsx b/app/web/src/utils/fileUtils.tsx similarity index 100% rename from frontend/src/utils/fileUtils.tsx rename to app/web/src/utils/fileUtils.tsx diff --git a/frontend/src/utils/formatBytes.ts b/app/web/src/utils/formatBytes.ts similarity index 100% rename from frontend/src/utils/formatBytes.ts rename to app/web/src/utils/formatBytes.ts diff --git a/frontend/src/utils/pathUtils.ts b/app/web/src/utils/pathUtils.ts similarity index 100% rename from frontend/src/utils/pathUtils.ts rename to app/web/src/utils/pathUtils.ts diff --git a/frontend/src/vite-env.d.ts b/app/web/src/vite-env.d.ts similarity index 100% rename from frontend/src/vite-env.d.ts rename to app/web/src/vite-env.d.ts diff --git a/frontend/tsconfig.json b/app/web/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to app/web/tsconfig.json diff --git a/frontend/tsconfig.node.json b/app/web/tsconfig.node.json similarity index 100% rename from frontend/tsconfig.node.json rename to app/web/tsconfig.node.json diff --git a/frontend/tsconfig.node.tsbuildinfo b/app/web/tsconfig.node.tsbuildinfo similarity index 100% rename from frontend/tsconfig.node.tsbuildinfo rename to app/web/tsconfig.node.tsbuildinfo diff --git a/frontend/tsconfig.tsbuildinfo b/app/web/tsconfig.tsbuildinfo similarity index 100% rename from frontend/tsconfig.tsbuildinfo rename to app/web/tsconfig.tsbuildinfo diff --git a/frontend/vite.config.d.ts b/app/web/vite.config.d.ts similarity index 100% rename from frontend/vite.config.d.ts rename to app/web/vite.config.d.ts diff --git a/frontend/vite.config.js b/app/web/vite.config.js similarity index 100% rename from frontend/vite.config.js rename to app/web/vite.config.js diff --git a/frontend/vite.config.ts b/app/web/vite.config.ts similarity index 100% rename from frontend/vite.config.ts rename to app/web/vite.config.ts diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 700a5e1..ed1f38e 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -29,7 +29,7 @@ Over time, it grew, I added auth, previews, signed URLs, and an admin UI. What b === "Single binary" - **Download** pre-built binaries from [GitHub Releases](https://github.com/heapoftrash/filetree/releases/latest) if available. If the release ships a UI bundle, place `frontend/dist` where the binary expects it (see [Installation](installation.md)), then: + **Download** pre-built binaries from [GitHub Releases](https://github.com/heapoftrash/filetree/releases/latest) if available. If the release ships a UI bundle, place it where the binary expects it (see [Installation](installation.md)), then: ```bash ROOT_PATH=./data ./filetree @@ -38,11 +38,11 @@ Over time, it grew, I added auth, previews, signed URLs, and an admin UI. What b Or build from source with the **embedded UI** (one file to run): ```bash - make build-frontend && make build-backend-embed - ROOT_PATH=./data ./backend/filetree + make build-frontend && make build-app-embed + ROOT_PATH=./data ./app/filetree ``` - Or `make build` and run `./backend/filetree` from the repo root so it serves `frontend/dist` from disk. + Or `make build` and run `./app/filetree` from the repo root so it serves `app/web/dist` from disk. For production: @@ -78,13 +78,13 @@ Over time, it grew, I added auth, previews, signed URLs, and an admin UI. What b git clone https://github.com/heapoftrash/filetree.git cd filetree ``` - Build the frontend and backend: + Build the UI and Go binary: ```bash make build ``` Run from the project root: ```bash - ROOT_PATH=./data ./backend/filetree + ROOT_PATH=./data ./app/filetree ``` Then open **http://localhost:8080**. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d16a9b3..b5436c5 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -5,7 +5,7 @@ icon: material/package-variant # Installation -Filetree runs as a single binary that serves both the API and the frontend. Prerequisites: **Go 1.21+** and **Node.js 18+** (Node is needed to build the frontend assets). +Filetree runs as a single binary that serves both the API and the web UI. Prerequisites: **Go 1.21+** and **Node.js 18+** (Node is needed to build the UI in `app/web/`). ## Installation @@ -15,15 +15,15 @@ Filetree runs as a single binary that serves both the API and the frontend. Prer ```bash make build - ROOT_PATH=./data ./backend/filetree + ROOT_PATH=./data ./app/filetree ``` - Then open **http://localhost:8080**. By default the binary serves the UI from `frontend/dist` when that folder exists relative to the working directory. For a **single binary** with the UI inside it, run `make build-frontend` and `make build-backend-embed` instead of `build-backend`, then run `./backend/filetree` (built with `-tags embed`). + Then open **http://localhost:8080**. By default the binary serves the UI from `app/web/dist` (repo root) or `./web/dist` when you run from the `app/` directory. For a **single binary** with the UI embedded, run `make build-frontend` then `make build-app-embed`, then run `./app/filetree`. For production, set `CONFIG_FILE` and `ROOT_PATH`: ```bash - ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./backend/filetree + ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./app/filetree ``` === "with Docker" @@ -53,40 +53,38 @@ Filetree runs as a single binary that serves both the API and the frontend. Prer === "from source" - Build the frontend and backend manually: + Build the UI and Go binary manually: ```bash - cd frontend && npm install && npm run build - cd .. - rm -rf backend/web/dist && cp -R frontend/dist backend/web/dist - cd backend && go mod tidy && go build -tags embed -o filetree . + cd app/web && npm install && npm run build + cd ../.. && cd app && go mod tidy && go build -tags embed -o filetree . ``` - Omit `-tags embed` and the `web/dist` copy if you prefer to run with `frontend/dist` on disk (e.g. run `./backend/filetree` from the repo root after only `go build -o filetree .`). + Omit `-tags embed` if you prefer to run with `app/web/dist` on disk only (e.g. `go build -o filetree .` and run `./app/filetree` from the repo root after `npm run build` in `app/web`). Run from the project root: ```bash - ROOT_PATH=./data ./backend/filetree + ROOT_PATH=./data ./app/filetree ``` Then open **http://localhost:8080**. === "development" - For local development with hot reload, run the backend and frontend in two terminals: + For local development with hot reload, run the Go server and the Vite dev server in two terminals: - **Terminal 1 — Backend:** + **Terminal 1 — API (Go):** ```bash - cd backend && go run . + cd app && go run . ``` API runs at **http://localhost:8080**. - **Terminal 2 — Frontend:** + **Terminal 2 — UI:** ```bash - cd frontend && npm install && npm run dev + cd app/web && npm install && npm run dev ``` - App runs at **http://localhost:5173** and proxies `/api` to the backend. + App runs at **http://localhost:5173** and proxies `/api` to the Go server. Open **http://localhost:5173** in your browser. Ensure `frontend.url` in config matches `http://localhost:5173` for CORS and OAuth. diff --git a/docs/getting-started/production.md b/docs/getting-started/production.md index e82d73d..02c4eb6 100644 --- a/docs/getting-started/production.md +++ b/docs/getting-started/production.md @@ -8,7 +8,7 @@ icon: material/application-export Build with `make build` (or see [Installation](installation.md)), then run: ```bash -ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./backend/filetree +ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./app/filetree ``` Then open **http://localhost:8080**. @@ -17,9 +17,9 @@ Then open **http://localhost:8080**. ### Option A: Binary -**Embedded UI (recommended for a single file to deploy):** from a source tree, run `make build-frontend` then `make build-backend-embed`. Deploy only `backend/filetree` (plus your config). The UI is baked into the binary via `go:embed` and `-tags embed`. +**Embedded UI (recommended for a single file to deploy):** from a source tree, run `make build-frontend` then `make build-app-embed`. Deploy only `app/filetree` (plus your config). The UI is baked into the binary via `go:embed` and `-tags embed` (Vite output is `app/web/dist` at compile time). -**UI beside the binary:** if you build with `make build` (or `go build` without `embed`), copy **`frontend/dist`** next to the runtime layout the binary expects (`./frontend/dist` or `../frontend/dist` relative to the process working directory), or run from the repository root after `make build`. +**UI on disk:** if you build with `make build` (or `go build` without `embed`), ensure `app/web/dist` exists — e.g. run from the repository root after `make build`, or run the binary from the `app/` directory with `./web/dist` present. Then: @@ -37,7 +37,7 @@ Then: ## Serving the UI -- **Embedded build** (`-tags embed`, with `backend/web/dist` populated at compile time): the backend serves the UI from memory; no `frontend/dist` folder at runtime. -- **Disk layout:** when `./frontend/dist` or `../frontend/dist` exists (from the process working directory), the backend serves those files instead. +- **Embedded build** (`-tags embed`, with `app/web/dist` populated at compile time): the server serves the UI from the binary; no separate static directory at runtime. +- **Disk layout:** when `./app/web/dist` or `./web/dist` exists (relative to the process working directory), those files are served instead. -No separate web server is required for the frontend. For HTTPS and custom domains, put a reverse proxy in front. +No separate web server is required for the UI. For HTTPS and custom domains, put a reverse proxy in front. From 20290629f06cfd19f6916f21b685de0d388d44e1 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 15:56:23 +0100 Subject: [PATCH 03/10] refactor(app): embed UI from app/uiembed with dist copy step Move go:embed and DistRoot into a Go-only package. Vite still outputs to app/web/dist; make embed-ui copies into app/uiembed/dist before go build -tags embed. CI stages the same way; Containerfile copies the Node stage output to ./uiembed/dist. Disk serving checks app/web/dist and app/uiembed/dist (and cwd-relative paths). Remove unused frontend-dist download from docker-push (image builds via multi-stage only). --- .dockerignore | 1 + .github/labeler.yml | 4 +--- .github/workflows/ci.yml | 14 +++++++------- .gitignore | 1 + CONTRIBUTING.md | 2 +- Containerfile | 2 +- Makefile | 14 ++++++++++---- README.md | 6 ++++-- app/frontend_mount.go | 6 +++--- app/main.go | 2 +- app/uiembed/.gitignore | 1 + app/{web => uiembed}/embed.go | 2 +- app/{web => uiembed}/embed_stub.go | 4 ++-- app/{web => uiembed}/fs.go | 4 ++-- docs/getting-started/index.md | 2 +- docs/getting-started/installation.md | 6 +++--- docs/getting-started/production.md | 8 ++++---- 17 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 app/uiembed/.gitignore rename app/{web => uiembed}/embed.go (82%) rename app/{web => uiembed}/embed_stub.go (58%) rename app/{web => uiembed}/fs.go (66%) diff --git a/.dockerignore b/.dockerignore index b1c5634..c6d499b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ docs node_modules app/web/node_modules app/web/dist +app/uiembed/dist app/filetree app/filetree.exe *.md diff --git a/.github/labeler.yml b/.github/labeler.yml index 75ae9a1..eefa055 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -11,9 +11,7 @@ Backend: - any-glob-to-any-file: app/frontend_mount.go - any-glob-to-any-file: app/go.mod - any-glob-to-any-file: app/go.sum - - any-glob-to-any-file: app/web/embed.go - - any-glob-to-any-file: app/web/embed_stub.go - - any-glob-to-any-file: app/web/fs.go + - any-glob-to-any-file: app/uiembed/** Frontend: - changed-files: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb7a10a..a1fba47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,13 @@ jobs: uses: actions/download-artifact@v4 with: name: frontend-dist - path: app/web/dist + path: . + + - name: Stage UI for embed + run: | + rm -rf app/uiembed/dist + mkdir -p app/uiembed/dist + cp -R app/web/dist/. app/uiembed/dist/ - name: Build with embedded frontend run: cd app && go build -tags embed -o filetree . @@ -167,12 +173,6 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Download frontend artifact - uses: actions/download-artifact@v4 - with: - name: frontend-dist - path: app/web/dist - - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.gitignore b/.gitignore index 3230e05..79cedf8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ app/data/ # Node / UI (also legacy paths) node_modules/ app/web/dist/ +app/uiembed/dist/ frontend/dist/ # MkDocs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ace8d37..a6c13b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Validate locally: `make commitlint` or `npx commitlint --last --verbose` 1. Create a branch from `main`. 2. Ensure `go build ./... && go vet ./... && go test ./...` passes in `app`. 3. Ensure `npm run build` passes in `app/web`. -4. Optional: `make build-app-embed` verifies `go build -tags embed` after `make build-frontend`. +4. Optional: `make build-app-embed` runs `embed-ui` (copies `app/web/dist` → `app/uiembed/dist`) then verifies `go build -tags embed`. 5. Open a PR with a clear description of the change. ## Code style diff --git a/Containerfile b/Containerfile index 965e8e5..3fdb12d 100644 --- a/Containerfile +++ b/Containerfile @@ -11,7 +11,7 @@ WORKDIR /app COPY app/go.mod app/go.sum ./ RUN go mod download COPY app/ ./ -COPY --from=frontend /app/web/dist ./web/dist +COPY --from=frontend /app/web/dist ./uiembed/dist RUN CGO_ENABLED=0 go build -tags embed -o filetree . FROM alpine:3.20 diff --git a/Makefile b/Makefile index c3d93b5..eb5c326 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-app build-frontend build-app-embed test commitlint clean run run-frontend +.PHONY: build build-app build-frontend embed-ui build-app-embed test commitlint clean run run-frontend # Build Go binary (output: app/filetree or app/filetree.exe) build-app: @@ -8,8 +8,14 @@ build-app: build-frontend: cd app/web && npm run build -# Single binary with embedded UI (Vite writes to app/web/dist; no copy step) -build-app-embed: build-frontend +# Copy Vite output into app/uiembed/dist for go:embed (Go-only package) +embed-ui: + rm -rf app/uiembed/dist + mkdir -p app/uiembed/dist + cp -R app/web/dist/. app/uiembed/dist/ + +# Single binary with embedded UI +build-app-embed: build-frontend embed-ui cd app && go build -tags embed -o filetree . # Build both Go binary and frontend assets @@ -38,4 +44,4 @@ run-frontend: # Remove build artifacts clean: rm -f app/filetree app/filetree.exe - rm -rf app/web/dist frontend/dist + rm -rf app/web/dist app/uiembed/dist frontend/dist diff --git a/README.md b/README.md index 29e7823..0a11abd 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ If the package is private, run `docker login ghcr.io` before `docker pull`. ### Production binary -**Embedded UI (single artifact)** — the Vite app lives in `app/web/` and writes to `app/web/dist`; build the frontend, then compile the Go app with the `embed` tag: +**Embedded UI (single artifact)** — the Vite app lives in `app/web/` and writes to `app/web/dist`. For embedding, that output is copied into `app/uiembed/dist` (Go-only package) before `go build -tags embed`. From the repo root: ```bash git clone https://github.com/heapoftrash/filetree.git @@ -79,9 +79,11 @@ make build-app-embed ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./app/filetree ``` +(`make build-app-embed` runs `embed-ui`, which performs the copy, then compiles.) + The **prebuilt container image** already uses an embedded UI (no separate static bundle directory in the image). -**UI on disk (optional)** — `make build` and run `./app/filetree` from the repo root; the binary still serves files from `app/web/dist` or `./web/dist` when you run it from the `app/` directory (see [documentation](https://heapoftrash.github.io/filetree/)). +**UI on disk (optional)** — `make build` and run `./app/filetree` from the repo root; the binary serves files from `app/web/dist` when present, or from `app/uiembed/dist` / `./web/dist` / `./uiembed/dist` depending on your working directory (see [documentation](https://heapoftrash.github.io/filetree/)). See [docs/API.md](docs/API.md) for the API. Build docs locally: `pip install mkdocs-material && mkdocs serve`. diff --git a/app/frontend_mount.go b/app/frontend_mount.go index 2c1ed4b..9d5f64c 100644 --- a/app/frontend_mount.go +++ b/app/frontend_mount.go @@ -7,7 +7,7 @@ import ( "os" "github.com/gin-gonic/gin" - "github.com/heapoftrash/filetree/app/web" + "github.com/heapoftrash/filetree/app/uiembed" ) func mountFrontend(r *gin.Engine) { @@ -18,7 +18,7 @@ func mountFrontend(r *gin.Engine) { } func mountEmbeddedFrontend(r *gin.Engine) bool { - distFS, ok := web.DistRoot() + distFS, ok := uiembed.DistRoot() if !ok { return false } @@ -45,7 +45,7 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { func mountDiskFrontend(r *gin.Engine) { var frontendDir string - for _, p := range []string{"./web/dist", "./app/web/dist"} { + for _, p := range []string{"./app/web/dist", "./app/uiembed/dist", "./web/dist", "./uiembed/dist"} { if _, err := os.Stat(p + "/index.html"); err == nil { frontendDir = p break diff --git a/app/main.go b/app/main.go index 0b84533..42441bb 100644 --- a/app/main.go +++ b/app/main.go @@ -83,7 +83,7 @@ func main() { } } - // Embedded UI (-tags embed + app/web/dist) or ./web/dist, ./app/web/dist on disk + // Embedded UI (-tags embed + app/uiembed/dist at compile time) or disk under ./app/web/dist, ./app/uiembed/dist, etc. mountFrontend(r) log.Printf("filetree API listening on :8080 (ROOT_PATH=%s)", cfg.Server.RootPath) diff --git a/app/uiembed/.gitignore b/app/uiembed/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/app/uiembed/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/app/web/embed.go b/app/uiembed/embed.go similarity index 82% rename from app/web/embed.go rename to app/uiembed/embed.go index 3d213b6..9b1c65c 100644 --- a/app/web/embed.go +++ b/app/uiembed/embed.go @@ -1,6 +1,6 @@ //go:build embed -package web +package uiembed import "embed" diff --git a/app/web/embed_stub.go b/app/uiembed/embed_stub.go similarity index 58% rename from app/web/embed_stub.go rename to app/uiembed/embed_stub.go index c190caf..d292d58 100644 --- a/app/web/embed_stub.go +++ b/app/uiembed/embed_stub.go @@ -1,8 +1,8 @@ //go:build !embed -package web +package uiembed import "embed" -// Dist is empty unless the binary is built with -tags embed and web/dist contains the Vite output. +// Dist is empty unless the binary is built with -tags embed and uiembed/dist contains the Vite output (copied from app/web/dist). var Dist embed.FS diff --git a/app/web/fs.go b/app/uiembed/fs.go similarity index 66% rename from app/web/fs.go rename to app/uiembed/fs.go index 6b510a4..cc357c3 100644 --- a/app/web/fs.go +++ b/app/uiembed/fs.go @@ -1,10 +1,10 @@ -package web +package uiembed import ( "io/fs" ) -// DistRoot returns an fs.FS rooted at the embedded or copied dist/ tree when index.html is present. +// DistRoot returns an fs.FS rooted at the embedded dist/ tree when index.html is present. func DistRoot() (fs.FS, bool) { sub, err := fs.Sub(Dist, "dist") if err != nil { diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index ed1f38e..9439e19 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -42,7 +42,7 @@ Over time, it grew, I added auth, previews, signed URLs, and an admin UI. What b ROOT_PATH=./data ./app/filetree ``` - Or `make build` and run `./app/filetree` from the repo root so it serves `app/web/dist` from disk. + Or `make build` and run `./app/filetree` from the repo root so it serves `app/web/dist` from disk (or `app/uiembed/dist` if you staged an embed tree there). For production: diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index b5436c5..159f54c 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -18,7 +18,7 @@ Filetree runs as a single binary that serves both the API and the web UI. Prereq ROOT_PATH=./data ./app/filetree ``` - Then open **http://localhost:8080**. By default the binary serves the UI from `app/web/dist` (repo root) or `./web/dist` when you run from the `app/` directory. For a **single binary** with the UI embedded, run `make build-frontend` then `make build-app-embed`, then run `./app/filetree`. + Then open **http://localhost:8080**. By default the binary serves the UI from `app/web/dist` (repo root) when present, or `app/uiembed/dist`, `./web/dist`, or `./uiembed/dist` depending on the working directory. For a **single binary** with the UI embedded, run `make build-frontend` then `make build-app-embed` (which copies the Vite build into `app/uiembed/dist` and compiles with `-tags embed`), then run `./app/filetree`. For production, set `CONFIG_FILE` and `ROOT_PATH`: @@ -57,10 +57,10 @@ Filetree runs as a single binary that serves both the API and the web UI. Prereq ```bash cd app/web && npm install && npm run build - cd ../.. && cd app && go mod tidy && go build -tags embed -o filetree . + cd ../.. && make embed-ui && cd app && go mod tidy && go build -tags embed -o filetree . ``` - Omit `-tags embed` if you prefer to run with `app/web/dist` on disk only (e.g. `go build -o filetree .` and run `./app/filetree` from the repo root after `npm run build` in `app/web`). + Omit `-tags embed` if you prefer to run with `app/web/dist` on disk only (e.g. `go build -o filetree .` and run `./app/filetree` from the repo root after `npm run build` in `app/web`). For an embedded build without Make, copy `app/web/dist` to `app/uiembed/dist` before `go build -tags embed`. Run from the project root: diff --git a/docs/getting-started/production.md b/docs/getting-started/production.md index 02c4eb6..c9a5414 100644 --- a/docs/getting-started/production.md +++ b/docs/getting-started/production.md @@ -17,9 +17,9 @@ Then open **http://localhost:8080**. ### Option A: Binary -**Embedded UI (recommended for a single file to deploy):** from a source tree, run `make build-frontend` then `make build-app-embed`. Deploy only `app/filetree` (plus your config). The UI is baked into the binary via `go:embed` and `-tags embed` (Vite output is `app/web/dist` at compile time). +**Embedded UI (recommended for a single file to deploy):** from a source tree, run `make build-frontend` then `make build-app-embed`. Deploy only `app/filetree` (plus your config). The UI is baked into the binary via `go:embed` and `-tags embed`. The build copies Vite output from `app/web/dist` into `app/uiembed/dist` so the embed package stays Go-only. -**UI on disk:** if you build with `make build` (or `go build` without `embed`), ensure `app/web/dist` exists — e.g. run from the repository root after `make build`, or run the binary from the `app/` directory with `./web/dist` present. +**UI on disk:** if you build with `make build` (or `go build` without `embed`), ensure `app/web/dist` exists — e.g. run from the repository root after `make build`, or use `./web/dist` / `./uiembed/dist` when running with working directory `app/`. Then: @@ -37,7 +37,7 @@ Then: ## Serving the UI -- **Embedded build** (`-tags embed`, with `app/web/dist` populated at compile time): the server serves the UI from the binary; no separate static directory at runtime. -- **Disk layout:** when `./app/web/dist` or `./web/dist` exists (relative to the process working directory), those files are served instead. +- **Embedded build** (`-tags embed`, with `app/uiembed/dist` populated at compile time): the server serves the UI from the binary; no separate static directory at runtime. +- **Disk layout:** if there is no usable embedded bundle (e.g. plain `go build` without `-tags embed`, or an empty embed), the server looks for `./app/web/dist`, `./app/uiembed/dist`, `./web/dist`, or `./uiembed/dist` (relative to the working directory) and serves those files. No separate web server is required for the UI. For HTTPS and custom domains, put a reverse proxy in front. From df1b09b274116ca36a4c42f2d373f77f20021067 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:00:07 +0100 Subject: [PATCH 04/10] build(make): default make build to embedded single binary build and all run build-frontend, embed-ui, and go build -tags embed. build-app remains for Go-only dev builds; build-app-embed aliases build. Update README and getting-started docs accordingly. --- CONTRIBUTING.md | 2 +- Makefile | 32 +++++++++++----------------- README.md | 9 +++----- docs/getting-started/index.md | 6 +++--- docs/getting-started/installation.md | 9 ++++---- docs/getting-started/production.md | 4 ++-- 6 files changed, 26 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6c13b6..23de5aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Validate locally: `make commitlint` or `npx commitlint --last --verbose` 1. Create a branch from `main`. 2. Ensure `go build ./... && go vet ./... && go test ./...` passes in `app`. 3. Ensure `npm run build` passes in `app/web`. -4. Optional: `make build-app-embed` runs `embed-ui` (copies `app/web/dist` → `app/uiembed/dist`) then verifies `go build -tags embed`. +4. Optional: `make build` verifies the full embedded binary (frontend + `go build -tags embed`). 5. Open a PR with a clear description of the change. ## Code style diff --git a/Makefile b/Makefile index eb5c326..67a2132 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,41 @@ -.PHONY: build build-app build-frontend embed-ui build-app-embed test commitlint clean run run-frontend +.PHONY: build all build-app build-frontend embed-ui build-app-embed test commitlint clean run run-frontend -# Build Go binary (output: app/filetree or app/filetree.exe) +# One binary with embedded UI: Vite → app/web/dist → app/uiembed/dist → go build -tags embed +build: build-frontend embed-ui + cd app && go build -tags embed -o filetree . + +all: build + +# Same as build (kept for scripts and docs that name it explicitly) +build-app-embed: build + +# Go only, no embed (dev: skip npm when UI unchanged; serve from app/web/dist on disk if present) build-app: cd app && go build -o filetree . -# Build frontend (output: app/web/dist/) +# Frontend (output: app/web/dist/) build-frontend: cd app/web && npm run build -# Copy Vite output into app/uiembed/dist for go:embed (Go-only package) +# Stage static files for go:embed (used by build) embed-ui: rm -rf app/uiembed/dist mkdir -p app/uiembed/dist cp -R app/web/dist/. app/uiembed/dist/ -# Single binary with embedded UI -build-app-embed: build-frontend embed-ui - cd app && go build -tags embed -o filetree . - -# Build both Go binary and frontend assets -all: build-app build-frontend - -# Alias -build: all - -# Validate last commit message (Conventional Commits) commitlint: npx commitlint --last --verbose -# Run tests test: cd app && go vet ./... && go test ./... cd app/web && npm run build -# Run Go server (dev) run: ./app/filetree -# Run frontend dev server (proxies /api to backend) run-frontend: cd app/web && npm run dev -# Remove build artifacts clean: rm -f app/filetree app/filetree.exe rm -rf app/web/dist app/uiembed/dist frontend/dist diff --git a/README.md b/README.md index 0a11abd..2052442 100644 --- a/README.md +++ b/README.md @@ -70,20 +70,17 @@ If the package is private, run `docker login ghcr.io` before `docker pull`. ### Production binary -**Embedded UI (single artifact)** — the Vite app lives in `app/web/` and writes to `app/web/dist`. For embedding, that output is copied into `app/uiembed/dist` (Go-only package) before `go build -tags embed`. From the repo root: +**Production binary** — `make build` runs the Vite production build, copies output into `app/uiembed/dist` for `go:embed`, and compiles **one** `app/filetree` binary with the UI embedded (install UI deps once: `cd app/web && npm ci`): ```bash git clone https://github.com/heapoftrash/filetree.git -make build-frontend -make build-app-embed +make build ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./app/filetree ``` -(`make build-app-embed` runs `embed-ui`, which performs the copy, then compiles.) - The **prebuilt container image** already uses an embedded UI (no separate static bundle directory in the image). -**UI on disk (optional)** — `make build` and run `./app/filetree` from the repo root; the binary serves files from `app/web/dist` when present, or from `app/uiembed/dist` / `./web/dist` / `./uiembed/dist` depending on your working directory (see [documentation](https://heapoftrash.github.io/filetree/)). +**UI on disk (optional)** — `make build-app` (Go only) plus `make build-frontend` if you want `app/web/dist` on disk; run `./app/filetree` from the repo root and the server will pick up `app/web/dist`, `app/uiembed/dist`, or cwd-relative paths (see [documentation](https://heapoftrash.github.io/filetree/)). See [docs/API.md](docs/API.md) for the API. Build docs locally: `pip install mkdocs-material && mkdocs serve`. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 9439e19..4555c31 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -35,14 +35,14 @@ Over time, it grew, I added auth, previews, signed URLs, and an admin UI. What b ROOT_PATH=./data ./filetree ``` - Or build from source with the **embedded UI** (one file to run): + Or build from source (**one embedded binary**): ```bash - make build-frontend && make build-app-embed + make build ROOT_PATH=./data ./app/filetree ``` - Or `make build` and run `./app/filetree` from the repo root so it serves `app/web/dist` from disk (or `app/uiembed/dist` if you staged an embed tree there). + For a **disk-only** UI (no embed), use `make build-app` and `make build-frontend`, then run `./app/filetree` from the repo root so it serves `app/web/dist` (or `app/uiembed/dist` if you ran `make embed-ui`). For production: diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 159f54c..7878e76 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -18,7 +18,7 @@ Filetree runs as a single binary that serves both the API and the web UI. Prereq ROOT_PATH=./data ./app/filetree ``` - Then open **http://localhost:8080**. By default the binary serves the UI from `app/web/dist` (repo root) when present, or `app/uiembed/dist`, `./web/dist`, or `./uiembed/dist` depending on the working directory. For a **single binary** with the UI embedded, run `make build-frontend` then `make build-app-embed` (which copies the Vite build into `app/uiembed/dist` and compiles with `-tags embed`), then run `./app/filetree`. + Then open **http://localhost:8080**. `make build` produces one binary with the UI embedded (Vite output is staged under `app/uiembed/dist` at compile time). With `make build-app` plus `make build-frontend`, the binary serves the UI from `app/web/dist` (repo root) or other disk paths — see [Production](production.md). For production, set `CONFIG_FILE` and `ROOT_PATH`: @@ -53,14 +53,13 @@ Filetree runs as a single binary that serves both the API and the web UI. Prereq === "from source" - Build the UI and Go binary manually: + From the repository root (install UI dependencies once: `cd app/web && npm ci`): ```bash - cd app/web && npm install && npm run build - cd ../.. && make embed-ui && cd app && go mod tidy && go build -tags embed -o filetree . + make build ``` - Omit `-tags embed` if you prefer to run with `app/web/dist` on disk only (e.g. `go build -o filetree .` and run `./app/filetree` from the repo root after `npm run build` in `app/web`). For an embedded build without Make, copy `app/web/dist` to `app/uiembed/dist` before `go build -tags embed`. + Without Make: after `npm run build` in `app/web`, run `make embed-ui` from the repo root, then `cd app && go build -tags embed -o filetree .`. For disk-only serving, use `go build` (no `-tags embed`) with `app/web/dist` present. Run from the project root: diff --git a/docs/getting-started/production.md b/docs/getting-started/production.md index c9a5414..a187f23 100644 --- a/docs/getting-started/production.md +++ b/docs/getting-started/production.md @@ -17,9 +17,9 @@ Then open **http://localhost:8080**. ### Option A: Binary -**Embedded UI (recommended for a single file to deploy):** from a source tree, run `make build-frontend` then `make build-app-embed`. Deploy only `app/filetree` (plus your config). The UI is baked into the binary via `go:embed` and `-tags embed`. The build copies Vite output from `app/web/dist` into `app/uiembed/dist` so the embed package stays Go-only. +**Embedded UI (recommended for a single file to deploy):** from a source tree, run **`make build`**. Deploy only `app/filetree` (plus your config). The UI is in the binary via `go:embed` and `-tags embed`; Make copies Vite output from `app/web/dist` into `app/uiembed/dist` before compiling. -**UI on disk:** if you build with `make build` (or `go build` without `embed`), ensure `app/web/dist` exists — e.g. run from the repository root after `make build`, or use `./web/dist` / `./uiembed/dist` when running with working directory `app/`. +**UI on disk:** use `make build-app` and `make build-frontend` (no `-tags embed`), or plain `go build` after `npm run build`, and ensure `app/web/dist` exists when you run from the repo root; from the `app/` directory, `./web/dist` / `./uiembed/dist` can be used instead. Then: From 9def44c5bd71012bafa2df2610db9d30b92bb402 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:05:29 +0100 Subject: [PATCH 05/10] fix(frontend): serve index.html on / without Gin StaticFile 301 StaticFile/StaticFileFS with path "/" returned 301 Location: "./", which causes broken redirects. Use explicit GET/HEAD handlers instead. --- app/frontend_mount.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/frontend_mount.go b/app/frontend_mount.go index 9d5f64c..de005f2 100644 --- a/app/frontend_mount.go +++ b/app/frontend_mount.go @@ -35,7 +35,12 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { c.FileFromFS(n, httpDist) }) } - r.StaticFileFS("/", "index.html", httpDist) + // Do not use StaticFileFS("/", ...): Gin returns 301 with Location: "./", which breaks clients. + serveIndex := func(c *gin.Context) { + c.FileFromFS("index.html", httpDist) + } + r.GET("/", serveIndex) + r.HEAD("/", serveIndex) r.NoRoute(func(c *gin.Context) { c.FileFromFS("index.html", httpDist) }) @@ -61,9 +66,14 @@ func mountDiskFrontend(r *gin.Engine) { if _, err := os.Stat(frontendDir + "/icon-light.svg"); err == nil { r.StaticFile("/icon-light.svg", frontendDir+"/icon-light.svg") } - r.StaticFile("/", frontendDir+"/index.html") + indexPath := frontendDir + "/index.html" + serveIndex := func(c *gin.Context) { + c.File(indexPath) + } + r.GET("/", serveIndex) + r.HEAD("/", serveIndex) r.NoRoute(func(c *gin.Context) { - c.File(frontendDir + "/index.html") + c.File(indexPath) }) log.Printf("Serving frontend from disk (%s)\n", frontendDir) } From d57fe398d6c7324867c736725c37e3b2592488b1 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:11:14 +0100 Subject: [PATCH 06/10] fix(frontend): avoid net/http index.html 301 for embedded SPA shell Gin FileFromFS rewrites the request path to "/index.html"; net/http FileServer then issues 301 Location "./" (stdlib special case). Serve index.html with fs.ReadFile + explicit headers instead. Add regression test for GET/HEAD /. --- app/frontend_mount.go | 23 ++++++++++++++++++++--- app/frontend_mount_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 app/frontend_mount_test.go diff --git a/app/frontend_mount.go b/app/frontend_mount.go index de005f2..98229fd 100644 --- a/app/frontend_mount.go +++ b/app/frontend_mount.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "os" + "strconv" "github.com/gin-gonic/gin" "github.com/heapoftrash/filetree/app/uiembed" @@ -35,14 +36,15 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { c.FileFromFS(n, httpDist) }) } - // Do not use StaticFileFS("/", ...): Gin returns 301 with Location: "./", which breaks clients. + // Do not use FileFromFS/StaticFile*/FileServer for index.html: Gin sets r.URL.Path to end with + // "/index.html", and net/http.FileServer then returns 301 Location: "./" (see net/http fs.go). serveIndex := func(c *gin.Context) { - c.FileFromFS("index.html", httpDist) + serveIndexHTML(c, distFS) } r.GET("/", serveIndex) r.HEAD("/", serveIndex) r.NoRoute(func(c *gin.Context) { - c.FileFromFS("index.html", httpDist) + serveIndexHTML(c, distFS) }) log.Println("Serving embedded frontend (built with -tags embed)") return true @@ -77,3 +79,18 @@ func mountDiskFrontend(r *gin.Engine) { }) log.Printf("Serving frontend from disk (%s)\n", frontendDir) } + +func serveIndexHTML(c *gin.Context, distFS fs.FS) { + b, err := fs.ReadFile(distFS, "index.html") + if err != nil { + c.Status(http.StatusNotFound) + return + } + c.Header("Content-Type", "text/html; charset=utf-8") + if c.Request.Method == http.MethodHead { + c.Header("Content-Length", strconv.Itoa(len(b))) + c.Status(http.StatusOK) + return + } + c.Data(http.StatusOK, "text/html; charset=utf-8", b) +} diff --git a/app/frontend_mount_test.go b/app/frontend_mount_test.go new file mode 100644 index 0000000..1b5ec50 --- /dev/null +++ b/app/frontend_mount_test.go @@ -0,0 +1,34 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + "testing/fstest" + + "github.com/gin-gonic/gin" +) + +func TestServeIndexHTMLNoRedirect(t *testing.T) { + gin.SetMode(gin.TestMode) + dist := fstest.MapFS{ + "index.html": &fstest.MapFile{Data: []byte("x")}, + } + for _, method := range []string{http.MethodGet, http.MethodHead} { + t.Run(method, func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(method, "http://example.com/", nil) + serveIndexHTML(c, dist) + if w.Code != http.StatusOK { + t.Fatalf("status: %d", w.Code) + } + if loc := w.Header().Get("Location"); loc != "" { + t.Fatalf("unexpected redirect Location %q (FileServer index.html bug)", loc) + } + if method == http.MethodGet && w.Body.Len() == 0 { + t.Fatal("expected body for GET") + } + }) + } +} From d36a4e521606a94b041f41552604729832e8d004 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:17:45 +0100 Subject: [PATCH 07/10] ci: download frontend artifact into app/web/dist for embed job upload-artifact archives paths relative to the uploaded directory, so extracting to the workspace root placed index.html/assets beside the repo instead of under app/web/dist. The embed copy then read an empty gitignored tree. Extract to app/web/dist to match make embed-ui. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1fba47..e3ac341 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,11 +126,13 @@ jobs: go-version: '1.25' cache-dependency-path: app/go.sum + # Upload stores paths relative to app/web/dist (index.html, assets/...), not app/web/dist/... + # Extract directly into app/web/dist so cp below finds the built UI. - name: Download frontend dist uses: actions/download-artifact@v4 with: name: frontend-dist - path: . + path: app/web/dist - name: Stage UI for embed run: | From 7f823874bc6bd8426a32659e92727013c202278b Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:28:58 +0100 Subject: [PATCH 08/10] test(app): cover dist routes for assets, icons, and SPA fallback Extract attachDistRoutes from mountEmbeddedFrontend so tests exercise the same registration as embed builds. Assert /assets, favicon, icon-light, /client routes, and no-assets layout. --- app/frontend_mount.go | 9 +++- app/frontend_mount_test.go | 88 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/app/frontend_mount.go b/app/frontend_mount.go index 98229fd..23c062a 100644 --- a/app/frontend_mount.go +++ b/app/frontend_mount.go @@ -23,6 +23,13 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { if !ok { return false } + attachDistRoutes(r, distFS) + log.Println("Serving embedded frontend (built with -tags embed)") + return true +} + +// attachDistRoutes serves a Vite-style dist tree (index.html, assets/, optional root SVGs) for tests and embed. +func attachDistRoutes(r *gin.Engine, distFS fs.FS) { httpDist := http.FS(distFS) if assetsFS, err := fs.Sub(distFS, "assets"); err == nil { r.StaticFS("/assets", http.FS(assetsFS)) @@ -46,8 +53,6 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { r.NoRoute(func(c *gin.Context) { serveIndexHTML(c, distFS) }) - log.Println("Serving embedded frontend (built with -tags embed)") - return true } func mountDiskFrontend(r *gin.Engine) { diff --git a/app/frontend_mount_test.go b/app/frontend_mount_test.go index 1b5ec50..c3456b8 100644 --- a/app/frontend_mount_test.go +++ b/app/frontend_mount_test.go @@ -3,6 +3,7 @@ package main import ( "net/http" "net/http/httptest" + "strings" "testing" "testing/fstest" @@ -32,3 +33,90 @@ func TestServeIndexHTMLNoRedirect(t *testing.T) { }) } } + +func TestAttachDistRoutes_AssetsAvailable(t *testing.T) { + gin.SetMode(gin.TestMode) + dist := fstest.MapFS{ + "index.html": &fstest.MapFile{Data: []byte(``)}, + "assets/main-abc.js": &fstest.MapFile{Data: []byte("export default function () {}")}, + "favicon.svg": &fstest.MapFile{Data: []byte("")}, + "icon-light.svg": &fstest.MapFile{Data: []byte("")}, + } + r := gin.New() + attachDistRoutes(r, dist) + + t.Run("GET asset", func(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/assets/main-abc.js", nil) + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("status: %d body=%q", w.Code, w.Body.String()) + } + if !strings.Contains(w.Body.String(), "export default") { + t.Fatalf("unexpected body: %q", w.Body.String()) + } + }) + + t.Run("GET favicon", func(t *testing.T) { + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/favicon.svg", nil)) + if w.Code != http.StatusOK { + t.Fatalf("status: %d", w.Code) + } + if !strings.Contains(w.Body.String(), "

")}, + "assets/chunk.js": &fstest.MapFile{Data: []byte("// chunk")}, + } + r := gin.New() + attachDistRoutes(r, dist) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/some/path", nil)) + if w.Code != http.StatusOK { + t.Fatalf("status: %d", w.Code) + } + if !strings.Contains(w.Body.String(), `id=root`) { + t.Fatalf("expected index shell, got: %q", w.Body.String()) + } +} + +func TestAttachDistRoutes_NoAssetsDirStillServesIndex(t *testing.T) { + gin.SetMode(gin.TestMode) + dist := fstest.MapFS{ + "index.html": &fstest.MapFile{Data: []byte("minimal")}, + } + r := gin.New() + attachDistRoutes(r, dist) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil)) + if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "minimal") { + t.Fatalf("GET /: %d body=%q", w.Code, w.Body.String()) + } + + // No assets/: StaticFS is not registered; unmatched /assets/* falls through to NoRoute (SPA shell). + w2 := httptest.NewRecorder() + r.ServeHTTP(w2, httptest.NewRequest(http.MethodGet, "/assets/missing.js", nil)) + if w2.Code != http.StatusOK || !strings.Contains(w2.Body.String(), "minimal") { + t.Fatalf("GET /assets/missing.js: %d body=%q", w2.Code, w2.Body.String()) + } +} From a0a2a87ff1040e7597e6fca175d9c2817972cc4b Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:34:52 +0100 Subject: [PATCH 09/10] fix: app logging --- app/frontend_mount.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend_mount.go b/app/frontend_mount.go index 23c062a..cffcead 100644 --- a/app/frontend_mount.go +++ b/app/frontend_mount.go @@ -24,7 +24,7 @@ func mountEmbeddedFrontend(r *gin.Engine) bool { return false } attachDistRoutes(r, distFS) - log.Println("Serving embedded frontend (built with -tags embed)") + log.Println("Serving embedded frontend") return true } From 6643506d9c608d0123951df378f0ccde018b3640 Mon Sep 17 00:00:00 2001 From: Manas Maiti Date: Wed, 25 Mar 2026 16:36:12 +0100 Subject: [PATCH 10/10] fix(make): depend embed-ui on build-frontend for make -j --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 67a2132..777bd5a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: build all build-app build-frontend embed-ui build-app-embed test commitlint clean run run-frontend # One binary with embedded UI: Vite → app/web/dist → app/uiembed/dist → go build -tags embed -build: build-frontend embed-ui +build: embed-ui cd app && go build -tags embed -o filetree . all: build @@ -17,8 +17,8 @@ build-app: build-frontend: cd app/web && npm run build -# Stage static files for go:embed (used by build) -embed-ui: +# Stage static files for go:embed (used by build). Prereqs build-frontend so `make -j` cannot copy before Vite finishes. +embed-ui: build-frontend rm -rf app/uiembed/dist mkdir -p app/uiembed/dist cp -R app/web/dist/. app/uiembed/dist/