S3 client
  • Svelte 41.8%
  • Rust 38.9%
  • TypeScript 17.6%
  • JavaScript 0.7%
  • Shell 0.5%
  • Other 0.5%
Find a file
Silbad c444b65e5c
All checks were successful
build / check (push) Successful in 5m16s
build / version (push) Successful in 4s
build / build (--runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis, src-tauri/target/x86_64-pc-windows-msvc/release/bundle, ghcr.io/catthehacker/ubuntu:act-24.04, windows, clang lld llvm nsis cmake ninja-build nasm pkg-config libayatana-appindic… (push) Successful in 19m55s
build / publish (push) Successful in 11s
build / build (--bundles deb,rpm,appimage, src-tauri/target/release/bundle, ghcr.io/catthehacker/ubuntu:act-22.04, linux, libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev xdg-utils) (push) Successful in 5m59s
build / check (release) Successful in 12m54s
build / version (release) Successful in 4s
build / build (--bundles deb,rpm,appimage, src-tauri/target/release/bundle, ghcr.io/catthehacker/ubuntu:act-22.04, linux, libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev xdg-utils) (release) Successful in 14m20s
build / build (--runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis, src-tauri/target/x86_64-pc-windows-msvc/release/bundle, ghcr.io/catthehacker/ubuntu:act-24.04, windows, clang lld llvm nsis cmake ninja-build nasm pkg-config libayatana-appindic… (release) Successful in 28m9s
build / publish (release) Successful in 11s
chore(release): bump to 1.0.1
2026-10-04 21:48:05 +02:00
.claude/skills chore(release): bump to 1.0.1 2026-10-04 21:48:05 +02:00
.dev chore(branding): redraw the logo and regenerate the app icons 2026-10-03 22:08:30 +02:00
.forgejo ci: build the Windows installer on Ubuntu 24.04 for NSIS 3.09 2026-10-02 20:50:59 +02:00
.vscode chore: configure git, editor and package manager 2026-09-19 00:03:36 +02:00
changelog chore(release): bump to 1.0.1 2026-10-04 21:48:05 +02:00
messages feat(ui): open the release notes from the version in the sidebar 2026-10-03 22:58:16 +02:00
project.inlang feat(i18n): translate the interface into six languages 2026-09-20 22:10:11 +02:00
scripts feat(core): translate every error the backend can emit 2026-09-29 23:43:12 +02:00
src feat(ui): link the project site in the sidebar footer 2026-10-04 21:41:14 +02:00
src-tauri chore(release): bump to 1.0.1 2026-10-04 21:48:05 +02:00
static fix(branding): show the redrawn logo in the sidebar and the favicon 2026-10-03 22:52:37 +02:00
.gitattributes chore: configure git, editor and package manager 2026-09-19 00:03:36 +02:00
.gitignore feat(i18n): translate the interface into six languages 2026-09-20 22:10:11 +02:00
.npmrc chore: configure git, editor and package manager 2026-09-19 00:03:36 +02:00
components.json refactor(ui): import the frontend library through #lib 2026-10-02 22:38:44 +02:00
package.json chore(release): bump to 1.0.1 2026-10-04 21:48:05 +02:00
pnpm-lock.yaml build(deps): upgrade to SvelteKit 3 2026-10-02 22:39:33 +02:00
README.md feat(ui): open the release notes from the version in the sidebar 2026-10-03 22:58:16 +02:00
renovate.json ci: check every change before building the installers 2026-09-29 23:43:18 +02:00
skills-lock.json chore(agents): add the shadcn-svelte skill for coding agents 2026-09-19 00:07:28 +02:00
tsconfig.json build(deps): upgrade to SvelteKit 3 2026-10-02 22:39:33 +02:00
vite.config.ts feat(ui): open the release notes from the version in the sidebar 2026-10-03 22:58:16 +02:00
vitest.config.ts refactor(ui): import the frontend library through #lib 2026-10-02 22:38:44 +02:00

Hangar

Desktop client for one or more S3-compatible object storages (AWS, MinIO, Cloudflare R2, Backblaze B2, Wasabi, Scaleway, OVHcloud, DigitalOcean Spaces, Ceph, Garage, Hetzner…).

Features

  • Connections: add / edit / delete, connection test, automatic provider detection from the endpoint, provider badge in the sidebar.
  • System keychain: credentials live in the OS keychain (Windows Credential Manager, macOS Keychain, Secret Service on Linux), one entry per connection, protected by the user session. No master password, nothing in clear text on disk, nothing in the logs.
  • Buckets: listing with versioning state (versioned / suspended / not versioned), creation with or without versioning, enable or suspend versioning on an existing bucket, deletion (refused unless empty).
  • Object Lock (WORM): lock state and default retention shown per bucket; creation with Object Lock, or enabling it on an existing versioned bucket (irreversible, explicit acknowledgement); default retention in governance or compliance mode (days or years). Per object or version: read the current retention and legal hold, set or extend a retention (presets or custom date), toggle the legal hold, shorten or remove a governance retention with the bypass option. Compliance rules are enforced before calling the server (only extendable, never governance again). The deletion analysis counts the protected versions the server will refuse and offers the governance bypass; every change is traced in the Audit.
  • Public access: each bucket shows whether it is private, public (read only) or open in a way Hangar does not manage (prefix, condition, write access). A bucket can be made public read only at creation or afterwards (explicit acknowledgement), and private again. Depending on the provider, Hangar adds or removes a single anonymous s3:GetObject statement in the bucket policy and keeps the others (AWS, MinIO, Wasabi, DigitalOcean Spaces, Hetzner, Ceph, others), or switches the bucket ACL (Backblaze B2). On AWS it lifts Block Public Access for the bucket and restores it when going private. Not offered on Cloudflare R2, Garage, OVHcloud and Scaleway, whose S3 API has no safe bucket-wide equivalent. Traced in the Audit.
  • Object browsing: folder navigation, pagination, filter, "versions" view (versions, latest version, delete markers) for versioned buckets, folder and bucket sizes on demand. Image thumbnails in the list and a larger preview on hover.
  • Protected deletion: selection, object, recursive folder or whole bucket. Preliminary analysis (count, size, preview), typed confirmation (exact name), explicit option to delete versions, extra acknowledgement above 10,000 items, server-side token (5 min), progress, cancellation. Every rule is enforced on the Rust side.
  • Upload of files and folders (buttons or drag & drop from the file manager) into the current folder. Multipart above 16 MiB (8 MiB parts in parallel, clean abort on error or cancellation), MIME type from the extension, 4 files in parallel.
  • Name collisions: server-side analysis before any transfer, then Rename (suffix "(1)", default) / Overwrite / Skip. On a versioned bucket, overwriting keeps the previous version. The choice is remembered.
  • Download: one object ("Save as" dialog), a selection or a whole folder (destination folder picker, hierarchy recreated), a specific version from the Versions view. Written to a .hangar-part temporary file renamed at the end; same collision policies for existing local files.
  • Share via link: temporary presigned URL for one object or one specific version (15 min, 1 h, 24 h or 7 days), copied to the clipboard, traced in the Audit (without the signed URL).
  • Transfers panel (bottom of the window, survives navigation): global and per-file progress, speed, time left, cancellation, retry of failures, error details.
  • Journal: rotating log files, shown in the app with an "Audit" tab for every sensitive operation, and "System" / "Interface" tabs to tell the Rust core's entries from the unexpected errors of the interface (render failures, unhandled rejections).
  • Documentation: an in-app page with two plain-language guides, "Understanding S3" (buckets, keys, versioning, Object Lock, temporary links, public buckets…) and "Using Hangar" (every screen, shortcuts, common errors), in the six languages.
  • Keyboard shortcuts: Del delete the selection, Ctrl+A select all, Esc deselect, Backspace parent folder, F5 refresh, Ctrl+U upload.
  • System integration: system tray icon (show / quit), optional "close to tray", optional start at login (starts hidden in the tray).
  • Settings backup: export the connections, the settings and the interface preferences (language, theme, table layouts…) to a JSON file, and import it on this computer or another one. The credentials are only included on demand, encrypted with a password (Argon2id + XChaCha20-Poly1305). The import previews the file and lets you pick what to apply; it never deletes anything, and a connection whose name already exists is kept or replaced as you choose. A wrong password changes nothing. Traced in the Audit.
  • Updates: Hangar checks the latest release at start and every 4 hours; when a newer version exists, an "Update to X.Y.Z" button appears in the sidebar above Journal. One click downloads the signed installer (progress bar), installs it and restarts. Nothing is installed otherwise. The version number at the bottom of the sidebar opens the changes of each version.
  • Languages: English, French, German, Spanish, Portuguese, Italian. The OS language is used by default (English when unavailable) and can be changed in the settings.
  • Light / dark theme, following the system by default.

Stack

  • Tauri 2 (Rust): all the logic — credentials, connections, S3 (aws-sdk-s3), deletion, transfers, settings, tray, logs. Plugins: log, opener, dialog, autostart, single-instance, updater, process. Credentials go through the keyring crate.
  • SvelteKit / Svelte 5: static SPA (adapter-static, ssr = false).
  • Tailwind CSS 4 + shadcn-svelte (Vega preset): the UI only uses shadcn-svelte components.
  • @tanstack/svelte-table v9 for the tables.
  • Paraglide JS (inlang) for localization.

Prerequisites

  • Node.js ≥ 20 and pnpm
  • Rust stable
  • Windows: Visual Studio Build Tools 2022 (C++ workload), WebView2 (bundled with Windows 11)
  • See the Tauri prerequisites for macOS / Linux

Commands

pnpm install          # JS dependencies
pnpm tdev             # desktop app in dev mode (hot reload), as "Hangar Dev" (see below)
pnpm tbuild           # installer in src-tauri/target/release/bundle
pnpm dev              # frontend alone in the browser (Tauri commands do not answer)
pnpm check            # messages, svelte-check, i18n:check and errors:check
pnpm i18n:check       # every locale defines exactly the keys of en.json
pnpm errors:check     # every error code the backend emits is translated in src/lib/api.ts
pnpm test             # frontend unit tests (vitest)
cd src-tauri && cargo clippy --all-targets -- -D warnings && cargo test

pnpm tdev runs with src-tauri/tauri.dev.conf.json merged in: identifier com.eliophot.hangar.dev, so the development build keeps its own configuration directory, keychain entries and autostart entry, and never touches those of the installed Hangar.

Content Security Policy

src-tauri/tauri.conf.json sets a CSP (csp for the bundle, devCsp with the Vite dev server allowed). Scripts and styles come from the bundle only; images may come from anywhere over http(s) because thumbnails are presigned S3 URLs; connect-src only allows the Tauri IPC. A change of CSP must be checked with pnpm tdev (no error in the WebView console, thumbnails displayed) and with a built installer.

Changelog

changelog/en.md lists the notable changes of each version (Keep a Changelog layout), and fr.md, de.md, es.md, pt.md and it.md translate it. The files are bundled into the app (src/lib/changelog.ts), which shows them in the user's language from the version number of the sidebar; a release missing from a translation falls back to English. Add each user-facing change under ## [Unreleased] in all six files, and rename that heading to ## [X.Y.Z] - YYYY-MM-DD in the release bump. pnpm test fails when a translation lists other releases, or another number of changes, than en.md.

Continuous integration

.forgejo/workflows/build.yml first runs a check job (pnpm check, pnpm test, pnpm audit, cargo audit, cargo clippy -D warnings, cargo test, then the S3 integration tests against a MinIO started in the job), then builds the Linux bundles (.deb, .rpm, .AppImage, x86_64) and the Windows installer (NSIS -setup.exe, cross-compiled with cargo-xwin, so no MSI) on a self-hosted Forgejo runner with the ubuntu-latest label. The jobs install their own toolchain in ghcr.io/catthehacker/ubuntu:act-22.04, so the Linux bundles run on glibc 2.35 and newer; only the Windows build uses act-24.04, for the NSIS 3.09 the Tauri installer script needs. Cargo runs with --locked, pnpm with --frozen-lockfile. renovate.json groups the dependency updates (Tauri, AWS SDK, Svelte).

  • Pull request: the check job only.
  • Push to main: version X.Y.Z-dev.<run number>, published as the dev pre-release, which is recreated on the pushed commit every time.
  • Release published with tag vX.Y.Z: the tag must match the version of src-tauri/tauri.conf.json (the job fails otherwise). The installers, their .sig files, the updater manifest latest.json and a SHA256SUMS.txt are attached to the release, which Forgejo shows as the latest one.

Updates

The in-app updater (tauri-plugin-updater) reads https://git.devbit.me/hangar/hangar/releases/download/latest/latest.json, the manifest .forgejo/scripts/publish.sh builds from the signed installers of the release (windows-x86_64 for the NSIS setup, linux-x86_64 for the AppImage, linux-x86_64-deb and linux-x86_64-rpm for the packages). Forgejo's "latest" ignores pre-releases, so the rolling "dev" build never updates anyone; a dev build itself is offered the latest release, its version being lower. Note the order download/latest: the GitHub form latest/download is a 404 on Forgejo.

Every installer is signed with a minisign key (bundle.createUpdaterArtifacts in tauri.conf.json); the app only installs a file whose signature matches the pubkey of tauri.conf.json. The key pair was generated with pnpm tauri signer generate -w ~/.tauri/hangar.key:

  • the private key is not in the repository. CI reads it from the TAURI_SIGNING_PRIVATE_KEY secret (the content of hangar.key) and its password from TAURI_SIGNING_PRIVATE_KEY_PASSWORD (the key is encrypted: without it the build fails). Losing either means generating a new pair, changing pubkey and asking every user to install the next version by hand once;
  • pnpm tbuild needs them as well, or the build fails on purpose: $env:TAURI_SIGNING_PRIVATE_KEY_PATH = "$HOME\.tauri\hangar.key" and $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD (PowerShell).

The updater is not started by pnpm tdev (the dev build is never installed). The frontend side lives in src/lib/stores/updater.svelte.ts (check at start and every 4 hours, download progress, refusal while transfers run) and the sidebar button in src/lib/components/app-sidebar.svelte.

Localization

Messages live in messages/<locale>.json (inlang message format v2, flat snake_case keys) and are compiled by Paraglide into src/lib/paraglide/ (generated, gitignored) on every dev/build and by pnpm i18n:compile. Compiler options are in project.inlang/paraglide.config.js. The first compilation downloads the inlang message-format plugin (then cached in node_modules/.cache).

English is the source of truth and the fallback. Backend errors carry a stable code translated by the frontend (ERRORS map in src/lib/api.ts); their English message is only a fallback. Log and audit lines are English and are not translated.

To add a language: declare it in project.inlang/settings.json, create messages/<code>.json from en.json, add its native name to LOCALE_NAMES in src/lib/stores/locale.svelte.ts, add the tray menu labels in src-tauri/src/tray.rs, then run pnpm i18n:check. The project skill .claude/skills/paraglide-js/SKILL.md documents the conventions.

Upgrading from 0.5 or earlier

Up to 0.5, credentials lived in a Stronghold vault protected by a master password. Since 0.6 they live in the OS keychain and the old vault is not migrated: on first launch Hangar deletes hangar.stronghold and salt.txt, keeps the connections, and each connection asks for its keys again (edit it once to enter them).

Tests

Unit tests run without any service: cargo test in src-tauri (paths, keys, policies, retention rules, audit log helpers…) and pnpm test at the root (formatting, preferences, error translation, the transfers store and its handling of events that arrive before the job is known).

Tests that need the machine or a server are marked #[ignore] and fail loudly, not silently, when their prerequisite is missing. src-tauri/tests/vault.rs stores, reads, replaces and deletes credentials in the real OS keychain under a throwaway service name (com.eliophot.hangar.test-*), and leaves nothing behind:

cd src-tauri && cargo test --test vault -- --ignored

S3 integration tests

src-tauri/tests/s3_minio.rs (buckets, listing, versions, deletion), src-tauri/tests/s3_transfers.rs (simple and multipart upload, collisions and policies, cancellation without orphan parts, download, prefix statistics, overwrite on a versioned bucket) and src-tauri/tests/s3_object_lock.rs (bucket with Object Lock, default retention, per-version retention and legal hold, deletion refused then bypassed) and src-tauri/tests/s3_public_access.rs (public then private again, anonymous downloads checked, foreign policy statements kept) exercise the S3 layer against a real endpoint. They are ignored by default and need these variables (CI sets them and starts a MinIO):

$env:HANGAR_TEST_ENDPOINT = "http://127.0.0.1:9000"
$env:HANGAR_TEST_ACCESS_KEY = "minioadmin"
$env:HANGAR_TEST_SECRET_KEY = "minioadmin"
cd src-tauri
cargo test --test s3_minio --test s3_transfers --test s3_object_lock --test s3_public_access -- --ignored --nocapture

They create hangar-test-* buckets and delete them at the end. Convenient local servers: RustFS (real authentication, versioning not functional) or moto (pip install "moto[server]", python -m moto.server -p 9100, accepts any credentials, full versioning).

Structure

messages/                 one JSON per locale (en is the source of truth)
project.inlang/           inlang project settings + Paraglide compiler options
src/                      SvelteKit frontend
  lib/api.ts              typed wrappers of the Tauri commands, error translation, native dialogs
  lib/types.ts            mirrors of the Rust structs
  lib/format.ts           locale-aware formatting (bytes, dates, numbers, durations)
  lib/prefs.ts            UI preferences (localStorage: sorting, columns, conflict policy)
  lib/object-lock.ts      Object Lock labels and retention date helpers
  lib/docs.ts             content of the Documentation page (sections, icons, message functions)
  lib/paraglide/          generated messages and runtime (gitignored)
  lib/stores/             state (runes): connections, UI, transfers, folder sizes, locale, updater
  lib/components/         screens and dialogs (sidebar, connection, deletion, conflicts…)
    data-table/           shared TanStack bricks (sortable header, column toggle, sort functions)
    object-table/         objects table (folder / object / version rows)
    bucket-table/         buckets table
  routes/                 / , /c/[connId] , /c/[connId]/b/[bucket] , /journal , /docs , /settings
src-tauri/src/
  vault.rs                credentials in the OS keychain (keyring)
  connections.rs          metadata (connections.json, no secrets)
  settings.rs             settings.json (close to tray)
  tray.rs                 system tray icon and menu
  providers.rs            known providers + detection
  s3/                     client, buckets (incl. versioning), objects, protected deletion
  s3/object_lock.rs       Object Lock: bucket configuration, per-version retention, legal hold
  s3/public_access.rs     public read access: bucket policy or bucket ACL, AWS Block Public Access
  s3/transfer.rs          common transfer model, progress, safe keys / paths
  s3/upload.rs            preflight (collisions) + execution (PutObject / multipart)
  s3/download.rs          preflight (local collisions) + execution (GetObject → temp file)
  s3/stats.rs             aggregated size of a prefix
  logging.rs              tauri-plugin-log + journal reading
  error.rs                AppError { code, message, params }
  commands.rs             commands exposed to the frontend
scripts/i18n-check.mjs    locale parity check

Local files

Content Location (Windows)
Connections (no secrets) %APPDATA%\com.eliophot.hangar\connections.json
Settings %APPDATA%\com.eliophot.hangar\settings.json
S3 credentials Windows Credential Manager, service com.eliophot.hangar
Logs %LOCALAPPDATA%\com.eliophot.hangar\logs\hangar.log
UI language and table layout WebView2 localStorage (hangar.locale, hangar.table.*)

Adding a shadcn-svelte component

pnpm dlx shadcn-svelte@latest add <component>