diff --git a/.dockerignore b/.dockerignore index 44cd24c..c6d499b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,11 @@ .github docs node_modules -backend/filetree -backend/filetree.exe +app/web/node_modules +app/web/dist +app/uiembed/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..eefa055 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,9 +4,21 @@ 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/uiembed/** 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 c4b89bd..e3ac341 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,24 +99,54 @@ 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 + + app-embed: + name: App (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: 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: app/web/dist + + - 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 . docker-build: name: Build runs-on: ubuntu-latest - needs: [commitlint, backend, gosec, frontend] + needs: [commitlint, app, gosec, frontend, app-embed] steps: - uses: actions/checkout@v6 @@ -145,12 +175,6 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Download frontend artifact - uses: actions/download-artifact@v4 - with: - name: frontend-dist - path: frontend/dist - - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -197,7 +221,7 @@ jobs: ci-success: name: CI success runs-on: ubuntu-latest - needs: [commitlint, backend, gosec, frontend, 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..79cedf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ # 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/ +app/uiembed/dist/ frontend/dist/ # MkDocs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1849789..23de5aa 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,10 @@ 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. Open a PR with a clear description of the change. +2. Ensure `go build ./... && go vet ./... && go test ./...` passes in `app`. +3. Ensure `npm run build` passes in `app/web`. +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 @@ -32,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 | |------|---------| @@ -45,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 5f89fd5..3fdb12d 100644 --- a/Containerfile +++ b/Containerfile @@ -1,22 +1,24 @@ 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/ ./ -RUN CGO_ENABLED=0 go build -o filetree . +COPY app/ ./ +COPY --from=frontend /app/web/dist ./uiembed/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..777bd5a 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,41 @@ -.PHONY: build build-backend build-frontend test commitlint clean run-backend run-frontend +.PHONY: build all build-app build-frontend embed-ui 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 . +# One binary with embedded UI: Vite → app/web/dist → app/uiembed/dist → go build -tags embed +build: embed-ui + cd app && go build -tags embed -o filetree . -# Build frontend (output: frontend/dist/) -build-frontend: - cd frontend && npm run build +all: build + +# Same as build (kept for scripts and docs that name it explicitly) +build-app-embed: build -# Build both backend binary and frontend assets -all: build-backend build-frontend +# 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 . + +# Frontend (output: app/web/dist/) +build-frontend: + cd app/web && npm run build -# Alias -build: all +# 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/ -# Validate last commit message (Conventional Commits) commitlint: npx commitlint --last --verbose -# 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: - ./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 + 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 e4c2698..2052442 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@

- - Go Report Card + + Go Report Card Build status @@ -70,21 +70,25 @@ If the package is private, run `docker login ghcr.io` before `docker pull`. ### Production binary +**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 -ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./backend/filetree +ROOT_PATH=/path/to/files CONFIG_FILE=./config.yaml ./app/filetree ``` -Ship `backend/filetree` and `frontend/dist`. The binary serves the built UI when `frontend/dist` is present. +The **prebuilt container image** already uses an embedded UI (no separate static bundle directory in the image). + +**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`. ### 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/app/frontend_mount.go b/app/frontend_mount.go new file mode 100644 index 0000000..cffcead --- /dev/null +++ b/app/frontend_mount.go @@ -0,0 +1,101 @@ +package main + +import ( + "io/fs" + "log" + "net/http" + "os" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/heapoftrash/filetree/app/uiembed" +) + +func mountFrontend(r *gin.Engine) { + if mountEmbeddedFrontend(r) { + return + } + mountDiskFrontend(r) +} + +func mountEmbeddedFrontend(r *gin.Engine) bool { + distFS, ok := uiembed.DistRoot() + if !ok { + return false + } + attachDistRoutes(r, distFS) + log.Println("Serving embedded frontend") + 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)) + } + 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) + }) + } + // 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) { + serveIndexHTML(c, distFS) + } + r.GET("/", serveIndex) + r.HEAD("/", serveIndex) + r.NoRoute(func(c *gin.Context) { + serveIndexHTML(c, distFS) + }) +} + +func mountDiskFrontend(r *gin.Engine) { + var frontendDir string + 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 + } + } + 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") + } + 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(indexPath) + }) + 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..c3456b8 --- /dev/null +++ b/app/frontend_mount_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "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") + } + }) + } +} + +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()) + } +} 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 73% rename from backend/main.go rename to app/main.go index 4723768..42441bb 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,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 + 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) if err := r.Run(":8080"); err != nil { 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/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/uiembed/embed.go b/app/uiembed/embed.go new file mode 100644 index 0000000..9b1c65c --- /dev/null +++ b/app/uiembed/embed.go @@ -0,0 +1,8 @@ +//go:build embed + +package uiembed + +import "embed" + +//go:embed all:dist +var Dist embed.FS diff --git a/app/uiembed/embed_stub.go b/app/uiembed/embed_stub.go new file mode 100644 index 0000000..d292d58 --- /dev/null +++ b/app/uiembed/embed_stub.go @@ -0,0 +1,8 @@ +//go:build !embed + +package uiembed + +import "embed" + +// 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/uiembed/fs.go b/app/uiembed/fs.go new file mode 100644 index 0000000..cc357c3 --- /dev/null +++ b/app/uiembed/fs.go @@ -0,0 +1,17 @@ +package uiembed + +import ( + "io/fs" +) + +// 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 { + return nil, false + } + if _, err := fs.Stat(sub, "index.html"); err != nil { + return nil, false + } + return sub, true +} diff --git a/app/web/.gitignore b/app/web/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/app/web/.gitignore @@ -0,0 +1 @@ +dist/ 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 d34a957..4555c31 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 it where the binary expects it (see [Installation](installation.md)), then: ```bash ROOT_PATH=./data ./filetree ``` - Or build from source: + Or build from source (**one embedded binary**): ```bash make build - ROOT_PATH=./data ./backend/filetree + ROOT_PATH=./data ./app/filetree ``` + 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: ```bash @@ -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 9dc1c87..7878e76 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**. The binary serves the frontend from `frontend/dist` when present. + 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`: ```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,36 +53,37 @@ Filetree runs as a single binary that serves both the API and the frontend. Prer === "from source" - Build the frontend and backend manually: + From the repository root (install UI dependencies once: `cd app/web && npm ci`): ```bash - cd frontend && npm install && npm run build - cd ../backend && go mod tidy && go build -o filetree . + make build ``` + 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: ```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 101994b..a187f23 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,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`**. 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:** 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: + +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 `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. -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 UI. For HTTPS and custom domains, put a reverse proxy in front.