a13n
AI 에이전트를 만들고 클라우드에서 실행합니다
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Built for people whose buckets already work from boto3 and who are tired of
clients that either cannot express their endpoint settings or drown them in
features for storage backends they will never use.
boto3.client("s3", ...) or Config(...) argument, and the form shows the
equivalent snippet as you type, so a working Python client can be transcribed
by hand and will work.Everything you would otherwise download to check. Most S3 tools show you a file name and a size. This one shows you the file.
The full feature tour is on the landing page.
s3://bucket/key,
bucket/key or a bare key; a bucket that is not this connection's is refused,
and an address that does not exist says so instead of failing silently.team-a/releases/ and another at team-b/releases/ and both are
first-class places to work. Recently used connections float to the top.Previews never download a whole file just to decide whether they can show it: the
app fetches at most 512 KB to classify content the extension does not identify
(audio and video are classified from the name alone and read no bytes), and media
streams through a custom s3stream:// protocol that supports HTTP ranges. A
viewer asks for a file in a couple of hundred tiny ranges, one after another, so
objects up to 64 MB are fetched once and answered from memory; larger ones are
proxied range by range, which is why a multi-gigabyte video seeks without being
downloaded.
| Content | Behaviour |
|---|---|
| PNG, JPEG, GIF, WebP, BMP, ICO, AVIF, SVG | rendered inline |
| MP3, WAV, OGG, Opus, M4A, AAC, FLAC | player inline, seeking works |
| MP4, WebM, MOV, MKV | player inline, seeking works |
| embedded viewer with zoom, fit-to-width and page navigation, fetched with ranged reads | |
| Markdown | rendered, with a Rendered / Raw switch |
| JSON, JSONL | pretty-printed and highlighted |
| CSV, TSV | parsed into a table (quotes and embedded newlines handled) |
| Parquet | schema, columns and the first rows as a table |
| code (Python, JS/TS, Rust, Go, Java, C/C++, YAML, TOML, SQL, shell, XML, CSS, …) | syntax highlighted |
| plain text | shown as-is, UTF-8 |
| everything else | refused with an explicit reason and a Download button |
[!NOTE] The preview limit lives in Settings (25 MB by default; 2 MB to 1 GB). Files above it stay listed and downloadable, but the preview panel explains the limit instead of loading them. Audio and video are exempt: they stream over ranged reads, so even a multi-gigabyte file plays without being downloaded. Parquet has an additional 64 MB read ceiling.
Find in a file. ⌘F in the preview column, or the magnifier in the
panel header, searches the text the preview loaded — plain text, code and JSON,
plus Markdown in the Raw view — and highlights every hit. Enter and
⇧Enter step through them, the bar counts "3 of 47", and the search stops
at 2,000 matches with a + on the count. Match Case, Match Whole Word and Regular
Expression are one click each; an invalid pattern is reported in the bar. Esc
closes the bar before it moves the keyboard anywhere else. Previews without text
to search — an image, a PDF, a rendered Markdown document — hand ⌘F
back to the folder filter. On Linux the count, stepping and scrolling work on any
WebKitGTK; painting the marks needs 2.44 or newer.
Markdown is rendered through marked and then sanitised by a small allow-list
walker in src/lib/previewText.ts: unknown tags are unwrapped,
script/iframe/object/embed/form never survive, on* attributes are
dropped and javascript: URLs are stripped. There is an end-to-end check for
exactly this.
Each saved connection pins one endpoint, one bucket and one prefix. The editor is deliberately a thin skin over boto3:
| App field | boto3 |
|---|---|
| Endpoint URL | endpoint_url= |
| Region | region_name= |
| Access key ID | aws_access_key_id= |
| Secret access key | aws_secret_access_key= |
| Session token | aws_session_token= |
| Bucket | Bucket= (ListObjectsV2, PutObject, …) |
| Prefix | Prefix= |
| Addressing style | Config(s3={"addressing_style": …}) |
| Signature version | Config(signature_version=…) |
VirtualHostDomainRequired. The
default is virtual; switch to path for providers that demand the bucket in
the URL path. The error itself carries a hint when this is the problem.s3v4 (the boto3 default) and v4 use the AWS SDK for
Rust. s3v2, the legacy HmacSHA1 scheme that some appliances still require, is
implemented in this repository because the AWS SDK no longer speaks it — see
src-tauri/src/sigv2.rs.0600 file inside the config
directory, and Settings says so explicitly rather than pretending otherwise.0600 inside a 0700 directory, atomically (write
to a temporary file, then rename), and a corrupt file is backed up instead of
being silently discarded.crypto::redact exists so diagnostics can be shared
safely.Config location:
~/Library/Application Support/dev.s3viewer.app/config.json${XDG_CONFIG_HOME:-~/.config}/dev.s3viewer.app/config.jsonOverride it with S3VIEWER_CONFIG_DIR (handy for tests).
Prebuilt releases are on the landing page or directly under Releases:
| Asset | Platform |
|---|---|
S3-Viewer.dmg | macOS 14.2+, universal (Apple Silicon and Intel) |
S3-Viewer.AppImage | Linux, portable |
S3-Viewer-amd64.deb | Linux, Debian/Ubuntu |
SHA256SUMS.txt | checksums for the three above |
On macOS, open the disk image, drag S3 Viewer into Applications, and add a
connection. On Linux the .deb installs through the package manager and the
AppImage just runs.
[!WARNING] The macOS build is not notarised by Apple yet, so the first launch needs one allowance: System Settings → Privacy & Security → Open Anyway, or:
xattr -dr com.apple.quarantine "/Applications/S3 Viewer.app"That prompt returns for each new version until the build is signed with a Developer ID. Release assets are named without a version on purpose, so
releases/latest/download/S3-Viewer.dmgalways points at the newest build.
Every launch asks GitHub Releases whether a newer version exists and, if it does,
offers to download and install it. On macOS the .app is replaced in place; on
Linux an AppImage updates itself, while a .deb install asks for the
administrator password and installs through pkexec. Both then restart into the
new version.
Bundles are signed with a minisign key baked into the app, so a tampered or
corrupted download is rejected instead of installed. The private key never lives
in the repo: CI reads it from the TAURI_SIGNING_PRIVATE_KEY secret, and a release
made without it fails on purpose. AGENTS.md spells out the whole
contract, including the version bump every release needs.
The app is usable end to end without a pointer. ←/→ move between the
connections, the list and the preview; Enter opens; Space toggles the cursor
row; Escape clears the selection. On macOS use ⌘, on Linux Ctrl.
| Action | Keys |
|---|---|
| Address bar | ⌘L |
| Filter the folder | ⌘F |
| Find in the preview | ⌘F in the preview column |
| Download the selection or the cursor row | ⌘D |
| Copy S3 URIs | ⌘⇧C |
| Upload files / a folder | ⌘U / ⌘⇧U |
| New window / new connection / new folder | ⌘N / ⌘K / ⌘⇧N |
| Properties / rename | ⌘I / F2 |
| Cut, copy, paste objects | ⌘X ⌘C ⌘V |
| Back / forward | ⌘[ / ⌘] (Linux: Alt+← / Alt+→) |
| Refresh the listing | ⌘R |
| Open the row menu | ⇧F10 or right-click |
⌘F does one of two things, depending on where the keyboard is: inside a preview
with searchable text it opens find-in-file, everywhere else it focuses the folder
filter.
The full map — columns, focus restoration, dialogs, menus, the PDF viewer, and
what is deliberately left to the operating system — is in
docs/keyboard.md.
Requirements: Rust (stable, 1.90+), Node.js with pnpm, and the platform toolchain.
pnpm install
pnpm build:frontend # vite build → dist/
pnpm tauri dev # run in development
CI=true pnpm tauri build # package for the current platform
Outputs land in src-tauri/target/release/bundle/:
dmg/S3 Viewer_<version>_<arch>.dmg and macos/S3 Viewer.appdeb/ and appimage/CI=true matters for the .dmg: without it the bundler asks Finder to lay out
the disk-image window, which fails when the terminal lacks Automation permission
(AppleEvent timed out). The .app is produced either way.
The builds are unsigned. Local builds are unaffected by the updater:
createUpdaterArtifacts is passed by CI rather than set in tauri.conf.json,
precisely so that a build without a signing key still works. To sign properly,
add a Developer ID and notarisation to src-tauri/tauri.conf.json.
Linux packages are built by .github/workflows/release.yml on ubuntu-22.04,
which is the practical baseline for a .deb users can install.
Three layers, all runnable locally:
cargo test --manifest-path src-tauri/Cargo.toml # Rust: crypto, store, S3, SigV2, previews
pnpm test:js # JS: formatting, CSV, boto3, text search,
# listing cache, versions, latest.json
node e2e/run.mjs # real window, real bucket
pnpm check is the strict local gate: frontend build, cargo fmt --check,
clippy -D warnings, Rust tests. It does not run pnpm test:js, so run both
before tagging.
tauri-driver cannot drive the app on macOS: WKWebView ships no WebDriver
implementation, so it refuses to start. Rather than fake the window, the app hosts
the test itself — with S3VIEWER_E2E=1 the real window boots, a scripted scenario
runs inside the real webview against the real Rust backend and a real bucket, and
the report is written to $S3VIEWER_E2E_OUT. Partial reports are published after
every check, so a hang names the check that hung.
node e2e/run.mjs seeds a disposable prefix, builds, launches, prints the report
and always deletes everything it created — including after a failure. A lock
file stops two concurrent runs from deleting each other's fixtures.
Credentials come from ~/.secrets/s3/s3-creds.json or from S3VIEWER_TEST_*
environment variables. The suite is hard-wired to a single bucket (e2e/creds.mjs)
and refuses to run against anything else. See
docs/e2e-gui-testing.md for the guardrails, how to
read a failure, and what the suite cannot test.
src/ Vue 3 + Pinia webview
App.vue shell: layout, boot, connection flows, dialogs
harness.ts e2e-only in-page harness (bundled with VITE_S3VIEWER_E2E=1)
components/
layout/ sidebar, header, breadcrumb, toolbar, resizers, transfers
browser/ virtualised object list and row
preview/ panel, find bar, shared text body, one renderer per media type
dialogs/ connection editor, settings, import, copy/move, updates
ui/ buttons, badges, fields, modal frame, toasts
stores/ ui, connections, objects (+ folder search), progress, preview find, updates,
toasts, dialogs
lib/ typed IPC bridge, formatting, key arithmetic, file kinds,
highlight.js setup, Markdown sanitiser, boto3 snippet,
find matcher, listing cache
composables/ dialogs, focus trap, global shortcuts, context menus,
IPC subscriptions, object actions (objectActions/ by domain)
types/ wire types mirroring the Rust Serialize shapes
src-tauri/src/
lib.rs Tauri builder and the s3stream:// range protocol
commands.rs the Tauri command surface
state.rs shared AppCore
s3/aws.rs ObjectStore over the AWS SDK (SigV4)
s3/v2.rs ObjectStore for the legacy SigV2 scheme
sigv2.rs the V2 canonical string and HmacSHA1 signature
preview.rs the preview policy: what is shown, and why not
parquet_preview.rs Parquet schema and rows
objects.rs listing, transfer and key arithmetic
store.rs, crypto.rs connections and sealed secrets
progress.rs transfer progress
e2e/ the self-hosted end-to-end harness
Two rules worth knowing before editing:
The ObjectStore trait has two implementations, selected by the connection's
signature version. Because async fn in traits is not dyn-compatible, dispatch
goes through a small AnyStore enum rather than trait objects.
A release is a tag push. CI checks that the tag matches tauri.conf.json, builds
both platforms, signs the .app, the AppImage and the .deb, writes
latest.json, and publishes the GitHub Release. Nothing else is needed — the app
picks the release up on its next launch.
# Bump the same version in all three files:
# src-tauri/tauri.conf.json
# src-tauri/Cargo.toml
# package.json
git commit -am "chore(release): 0.2.0"
git tag v0.2.0
git push origin main v0.2.0
The tag must be exactly v plus that version. pnpm test:js asserts the three
files agree, and CI fails if the tag disagrees, because an installed copy would
otherwise install the "new" version and then be offered it again on every launch.
Use semver; v0.2.0 inside the config is not valid semver and breaks the
comparison.
The signing key is a repository secret: Settings → Secrets and variables →
Actions → TAURI_SIGNING_PRIVATE_KEY, holding the single-line content of
~/.tauri/s3-viewer.key. It is never committed, and losing it means no installed
copy can be updated again. The public half is plugins.updater.pubkey in
src-tauri/tauri.conf.json; changing it orphans every user.
latest.json lists every platform with the signature of its bundle; the app
fetches it through releases/latest/download/latest.json. Drafts and prereleases
are invisible to the updater. The full release checklist, the four platform keys,
the asset-name invariants and the verification steps live in
AGENTS.md.
Issues and pull requests are welcome. Before changing the updater, signing, or the
release workflow, read AGENTS.md — it documents the invariants that
keep installed copies updatable.
MIT © Gregory Koganovsky. See LICENSE.