Camilo Hollanda 5d33382e30 feat: bulk actions — recursively delete folders (key prefixes) (#68)
* feat(backend,frontend): recursive delete of key prefixes in bulk actions

Bulk object selection previously only supported deleting individually
listed object keys — folders ("prefixes") could not be selected or
deleted, leaving no way to remove a directory and all of its contents.

Backend:
- Add S3Service.DeleteObjectsByPrefix, which recursively lists every
  object under a prefix and batch-deletes them, returning the count.
- Extend the delete-multiple endpoint to accept a "prefixes" array
  alongside "keys"; keys are batch-deleted and each prefix is deleted
  recursively. Response now reports the total objects removed.

Frontend:
- Enable folder checkboxes and add a per-folder "Delete folder" action.
- Select-all now covers both files and folders.
- Route all bulk/folder deletes through a confirmation dialog that
  spells out the file/folder counts and warns that folders are removed
  recursively (previously bulk delete fired with no confirmation).
- api/hook send "prefixes"; optimistic update drops objects under any
  deleted prefix.

* fix(backend): validate delete prefixes and count actual removals

Addresses maintainer review on the recursive prefix-delete endpoint:

- Reject blank/whitespace-only prefixes with a 400 instead of a 500, and
  normalize each prefix to have a trailing "/" so "photos/2024" can no
  longer also delete siblings like "photos/2024-old/..." on this
  irreversible public endpoint.
- DeleteMultipleObjects now returns the number of objects actually
  removed (requested keys minus failures) rather than assuming every
  requested key was deleted; the handler sums real counts across the
  keys and prefix paths. Draining the full RemoveObjects error channel
  also fixes a potential sender-goroutine leak on early return.
- Add tests: blank-prefix -> 400, prefix trailing-slash normalization,
  and S3Service.DeleteObjectsByPrefix (list-then-delete, empty prefix,
  no-match, and list-error propagation).

* fix(frontend): align select-all with the active search filter

The header "select all" checkbox derived its checked state from the
filtered (searched) rows, but handleSelectAll operated on the full,
unfiltered object list — so with a search active it selected hidden
items and the checkbox state disagreed with the selection.

ObjectsTable now passes the keys of the currently visible (filtered)
rows to onSelectAll, and its checked state reflects whether every
visible row is selected. handleSelectAll toggles only those visible
rows, leaving any off-screen selection intact.

* fix(frontend): scope select-all to the visible page

After merging upstream's client-side deep-search pagination, the rendered
rows are pageObjects (one page slice) while select-all still operated on
filteredObjects (every match across hidden pages). Scope the header
checkbox's state and its select-all action to pageObjects so one click
never selects off-screen rows for a destructive bulk delete. In
normal/prefix browsing pageObjects === filteredObjects, so behavior there
is unchanged.

---------

Authored-by: Camilo Hollanda <775409+prem-prakash@users.noreply.github.com>
2026-07-14 23:11:23 +02:00
2026-07-11 22:53:40 +02:00
2026-07-11 17:59:34 +02:00
2025-11-24 15:00:08 +01:00
2026-07-11 22:53:40 +02:00

Docker Build Helm Chart Coverage License: MIT Go Version Artifact Hub

Garage UI - Web Dashboard for Garage S3 Storage

A modern web interface to manage Garage object storage clusters. Browse buckets, manage access keys, monitor your cluster, all from your browser.


Dashboard Buckets
Cluster Access Control

Features

  • Bucket management - create, configure, and browse buckets with drag-and-drop file uploads
  • Access key management - create keys, assign per-bucket permissions
  • Cluster overview - monitor node status, layout configuration, and storage usage
  • Flexible authentication - no auth, basic credentials, or OIDC (Keycloak, Authentik, etc.)
  • Multi-user access control - optional OIDC-team-based permissions, see docs/access-control.md
  • Easy deployment - single Docker image or Helm chart, configure with one YAML file

Quick Start

Prerequisites

  • Docker & Docker Compose
  • A running Garage cluster (v2.1.0+) - setup guide if you need one

1. Clone & Configure

git clone https://github.com/Noooste/garage-ui.git
cd garage-ui
cp config.example.yaml config.yaml

Edit config.yaml with your Garage endpoints and admin token (from garage.toml).

2. Start

docker compose up -d garage-ui

Access at http://localhost:8080

Deployment

Docker

docker run -d -p 8080:8080 \
  -v $(pwd)/config.yaml:/app/config.yaml \
  noooste/garage-ui:latest

Kubernetes

helm repo add garage-ui https://helm.noste.dev/
helm install garage-ui garage-ui/garage-ui \
  --set garage.endpoint=http://garage:3900 \
  --set garage.adminEndpoint=http://garage:3903 \
  --set garage.adminToken=your-token

The chart creates a ClusterIP service on port 80. To try it out before setting up an ingress:

kubectl port-forward svc/garage-ui 8080:80

Then open http://localhost:8080

Reusing your garage.toml

If you already have a running Garage instance, you can point Garage UI straight at your garage.toml and skip config.yaml entirely:

./garage-ui --garage-toml /etc/garage.toml

Garage UI reads the S3 endpoint, admin endpoint, admin token, and S3 region straight from the TOML file. When no authentication method is explicitly configured, token auth auto-enables: the login page asks for the Garage admin token, giving you a login wall with zero extra config.

Bind address handling: Wildcard addresses like 0.0.0.0 or [::] are converted to 127.0.0.1 so the UI can reach Garage on localhost. Inside a container this won't work, so override the endpoints explicitly with environment variables or a config file.

Docker:

docker run -d -p 8080:8080 \
  -v /etc/garage.toml:/etc/garage.toml:ro \
  -e GARAGE_UI_GARAGE_TOML=/etc/garage.toml \
  -e GARAGE_UI_GARAGE_ENDPOINT=http://garage:3900 \
  -e GARAGE_UI_GARAGE_ADMIN_ENDPOINT=http://garage:3903 \
  noooste/garage-ui:latest

The endpoint overrides are needed because the container cannot reach 127.0.0.1 on the host.

Combining flags: Use --garage-toml for Garage connection values and --config for everything else (auth, CORS, logging, etc.):

./garage-ui --garage-toml /etc/garage.toml --config config.yaml

Precedence order (highest wins): built-in defaults < garage.toml < config.yaml < environment variables.

Configuration

Minimum required config:

server:
  port: 8080

garage:
  endpoint: "http://garage:3900"
  admin_endpoint: "http://garage:3903"
  admin_token: "your-admin-token"
  region: "garage"

Server bind host is configured by server.host (default: ::). IPv6 literals like :: and ::1 are supported.

server:
  host: "::" # IPv6 wildcard (dual-stack-preferred)
  port: 8080

If your environment needs explicit IPv4-only binding, set server.host: "0.0.0.0".

See config.example.yaml for all options including authentication, CORS, and logging.

Environment Variables

Override any config value with GARAGE_UI_ prefix:

GARAGE_UI_SERVER_PORT=8080
GARAGE_UI_GARAGE_ENDPOINT=http://garage:3900
GARAGE_UI_GARAGE_ADMIN_TOKEN=your-token

Loading sensitive values from files (_FILE suffix)

For Docker and Kubernetes secrets, sensitive env vars can be read from files instead of plain values. Set {VAR}_FILE=/path/to/file and garage-ui uses the file's contents (trailing CR/LF trimmed) as the value. If both {VAR} and {VAR}_FILE are set, _FILE wins and a warning is logged. A missing or unreadable file stops startup.

Supported vars:

  • GARAGE_UI_GARAGE_ADMIN_TOKEN_FILE
  • GARAGE_UI_AUTH_ADMIN_USERNAME_FILE
  • GARAGE_UI_AUTH_ADMIN_PASSWORD_FILE
  • GARAGE_UI_AUTH_JWT_PRIVATE_KEY_FILE
  • GARAGE_UI_AUTH_OIDC_CLIENT_ID_FILE
  • GARAGE_UI_AUTH_OIDC_CLIENT_SECRET_FILE

Example with Docker Compose secrets:

services:
  garage-ui:
    image: noooste/garage-ui:latest
    environment:
      GARAGE_UI_AUTH_ADMIN_PASSWORD_FILE: /run/secrets/admin_password
    secrets:
      - admin_password

secrets:
  admin_password:
    file: ./admin_password.txt

This matches the convention used by the official Postgres and MySQL Docker images. Helm users don't need it; the chart already injects secrets via existingSecret references.

Garage Configuration

Garage UI requires these settings in your garage.toml:

# Admin API (required for Garage UI)
[admin]
api_bind_addr = "0.0.0.0:3903"  # Default: 127.0.0.1:3903
admin_token = "your-admin-token" # Generate with: openssl rand -base64 32

# S3 API
[s3_api]
s3_region = "garage"             # Default: "garage"
api_bind_addr = "[::]:3900"      # Default: 127.0.0.1:3900

Important: The admin_token and s3_region in garage.toml must match your Garage UI config.yaml.

For complete Garage configuration, see the official documentation.

Development

Backend (Go 1.25+):

cd backend
go run main.go --config ../config.yaml

Frontend (Node.js 25+):

cd frontend
npm install
npm run dev

API docs: http://localhost:8080/api/v1/

Troubleshooting

Connection failed:

curl http://localhost:3903/status -H "Authorization: Bearer your-token"

Enable debug logs:

logging:
  level: "debug"
  format: "text"  # or "json"

Roadmap

Roughly ordered by value. Open an issue to push something up the list.

  • Fine-grained access control: OIDC teams with per-bucket-prefix permissions, see docs/access-control.md
  • Object search: recursive substring search across a bucket
  • Bucket quotas: size and object count limits from bucket settings
  • Zero-config startup: run straight from garage.toml, log in with the admin token
  • Broad compatibility: Garage v1 through latest, IPv6-only networks, secrets from files
  • Inline object preview: images, video, PDF, and text without downloading (#60)
  • Presigned share links: time-limited download links from the object browser
  • Resumable uploads: multipart uploads that survive a dropped connection
  • Visual layout editor: staged vs. applied diff before committing layout changes
  • Admin audit log: who changed what, building on access control
  • Table and detail polish: sortable columns, clearer node details (#36, #37)

License

MIT - see LICENSE


Made with ❤️ in France 🇫🇷

S
Description
Garage Admin UI with OIDC and team access control
Readme MIT 3.2 MiB
Languages
Go 65.5%
TypeScript 32.9%
CSS 0.5%
Makefile 0.4%
JavaScript 0.3%
Other 0.4%