- Svelte 41.8%
- Rust 38.9%
- TypeScript 17.6%
- JavaScript 0.7%
- Shell 0.5%
- Other 0.5%
|
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
|
||
|---|---|---|
| .claude/skills | ||
| .dev | ||
| .forgejo | ||
| .vscode | ||
| changelog | ||
| messages | ||
| project.inlang | ||
| scripts | ||
| src | ||
| src-tauri | ||
| static | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| components.json | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| renovate.json | ||
| skills-lock.json | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
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:GetObjectstatement 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-parttemporary 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:
Deldelete the selection,Ctrl+Aselect all,Escdeselect,Backspaceparent folder,F5refresh,Ctrl+Uupload. - 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 thekeyringcrate. - 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
checkjob only. - Push to
main: versionX.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 ofsrc-tauri/tauri.conf.json(the job fails otherwise). The installers, their.sigfiles, the updater manifestlatest.jsonand aSHA256SUMS.txtare 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_KEYsecret (the content ofhangar.key) and its password fromTAURI_SIGNING_PRIVATE_KEY_PASSWORD(the key is encrypted: without it the build fails). Losing either means generating a new pair, changingpubkeyand asking every user to install the next version by hand once; pnpm tbuildneeds 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>