Skip to content

anantadwi13/explorer

Repository files navigation

explorer

Go version License: MIT

Playground / personal use only. This is a hobby project, not a hardened service. There is no authentication, no rate limiting, and no security audit. Anyone who can reach the port can read every file under the served directory. Run it on your own machine bound to loopback (the default), or on a trusted LAN — never expose it to the public internet, behind a tunnel, or on a shared host.

A single-binary local directory browser — point it at a folder, open the URL, read everything in place from your phone on the same Wi-Fi.

explorer on the desktop: browse a git repo, open a markdown file, switch to dark theme, preview a binary, drill into a commit diff, and inspect per-file blame from the file viewer

explorer on mobile: open a markdown file, scroll, browse the tree, jump to the commits list via the branch picker, and switch a text file to its blame view

Why explorer?

Sometimes you just want to read the markdown notes, text files, and images on your laptop from the couch — on a phone, in a browser, without copying anything anywhere. explorer is a single static Go binary that serves a chosen directory over HTTP as a read-only browser, with markdown rendering, inline images, and deep-linkable URLs. No accounts, no writes, no cloud.

Install

Download the binary (fastest)

Bare binary, no extract step. Replace <TAG> with the latest release version from the Releases page (e.g. v0.2.0).

# linux/amd64
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_amd64 && chmod +x explorer && ./explorer ./

# linux/arm64
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_arm64 && chmod +x explorer && ./explorer ./

# darwin/amd64 (Intel Macs)
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_amd64 && chmod +x explorer && ./explorer ./

# darwin/arm64 (Apple Silicon)
curl -L -o explorer https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_arm64 && chmod +x explorer && ./explorer ./

Windows (PowerShell):

Invoke-WebRequest -Uri https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_windows_amd64.exe -OutFile explorer.exe
.\explorer.exe .

Download the archive

Same binary inside an archive. The unix archive preserves the executable bit, so no chmod is needed after extracting.

# linux/amd64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_amd64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./

# linux/arm64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_linux_arm64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./

# darwin/amd64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_amd64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./

# darwin/arm64
curl -L -o explorer.tar.gz https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_darwin_arm64.tar.gz && tar -xzf explorer.tar.gz && ./explorer ./

Windows (PowerShell):

Invoke-WebRequest -Uri https://github.com/anantadwi13/explorer/releases/download/<TAG>/explorer_<TAG>_windows_amd64.zip -OutFile explorer.zip
Expand-Archive .\explorer.zip -DestinationPath .
.\explorer.exe .

Verify the download

Each release ships a single checksums.txt covering both bare binaries and archives. --ignore-missing lets the verify step succeed for whichever subset you actually downloaded.

# linux
sha256sum --ignore-missing -c checksums.txt

# macOS
shasum -a 256 --ignore-missing -c checksums.txt

After running the binary, confirm what you have with ./explorer --version — it prints the embedded tag, short commit, and build date in <tag> (commit <short-sha>, built <yyyy-mm-dd>) form.

macOS Gatekeeper: the first launch may show "cannot be opened because the developer cannot be verified" — these binaries are not signed/notarised. Clear the quarantine attribute once with:

xattr -d com.apple.quarantine ./explorer

This applies to both the bare binary and the binary extracted from the archive.

Windows SmartScreen: the first launch may show "Windows protected your PC". Click More info → Run anyway. Same caveat applies to both shapes — the binaries are not Authenticode-signed.

With Go (recommended for users with a Go toolchain)

Requires Go 1.24+.

go install github.com/anantadwi13/explorer/cmd/explorer@latest

This drops an explorer binary in $GOBIN (or $GOPATH/bin — make sure it is on your PATH). Pin a specific version with @v0.3.0 instead of @latest if you prefer.

To try it without installing:

go run github.com/anantadwi13/explorer/cmd/explorer@latest <dir>

From source (for contributors)

Requires Go 1.24+ and Node 18+.

git clone https://github.com/anantadwi13/explorer.git
cd explorer
make build      # builds the SPA, embeds it, produces ./explorer
./explorer <dir>

Docker / Docker Compose

docker compose up --build

Then open http://localhost:8080. The docker-compose.yml bind-mounts the current directory into /data (read-only) and runs:

explorer /data --host 0.0.0.0 --port 8080

--host 0.0.0.0 is mandatory inside the container. With the default 127.0.0.1, the server listens only on the container's loopback interface and Docker's port mapping cannot reach it.

Quick start

# Serve the current directory on the default loopback port
explorer .

# Serve /home/alice/docs on port 9000
explorer /home/alice/docs --port 9000

# Expose on all interfaces (LAN-accessible — read the warning below)
explorer . --host 0.0.0.0

When --host is not a loopback address, the startup banner prints a warning:

Output:

explorer (playground build — no auth, no audit; do not expose publicly)
explorer serving /home/alice/docs
  → http://0.0.0.0:8080
  ⚠ host is not loopback — anyone on this network can read these files.

Configuration

Flag Default Description
--port 8080 TCP port to listen on
--host 127.0.0.1 Address to bind to
--version, -v Print <version> (commit <short-sha>, built <yyyy-mm-dd>) and exit

Features

  • Serves any local directory over HTTP as a read-only browser
  • Embedded React SPA: markdown rendering (GFM), syntax-highlighted text viewer for 35+ languages (Go, TypeScript, Python, Kotlin, Swift, Rust, JSON, TOML, and more), inline images, download button for everything else
  • Mobile-first responsive layout; desktop shows a persistent tree sidebar
  • Deep-linkable URLs for every folder and file
  • Light / dark / system theme toggle
  • Single static binary — no runtime dependencies
  • Git browsing when the served root is a git repository: commit history, branch/tag picker, per-commit diffs (lazy hunk loading), working-tree status, per-file history, and line-by-line blame

Git browsing

When explorer is pointed at a directory whose top level contains a .git entry (either a directory or a worktree pointer file), it exposes a small read-only git surface alongside the file browser:

  • Branch picker in the toolbar (desktop) or drawer header (mobile) — switch between branches and tags, jump to local changes.
  • Commits list at /git/commits (or /git/commits?ref=<branch-or-tag>) — paginated by cursor, newest first.
  • Commit detail at /git/commits/<sha> — header, parents, file list (paginated 50 at a time), per-file diffs lazily loaded in 3-hunk batches.
  • Working-tree status at /git/status — staged / unstaged / untracked files; first three diffs auto-expand.
  • Per-file last-commit strip above every file viewer when the served root is a repo — short_sha · subject · author · time, with a Modified locally pill when the file is dirty, an Untracked pill when it isn't tracked, and a History link to the path-filtered commits list.
  • Code | Blame tabs on text files — switch the file view to ?view=blame to see per-line authorship grouped into commit chunks. Locally-modified lines are marked as Working tree · uncommitted and styled distinctly so the dirty overlay is unambiguous.
  • Path-filtered commits at /git/commits?path=<file> — sticky banner naming the filter with a [× clear] affordance; the same Modified locally / Untracked pill from the file viewer appears as a status row.

The capability is strictly gated on a .git entry at the served root itself — ancestor .git directories are not walked. When the gate is not satisfied, every /api/git/* endpoint returns not_a_repo and the SPA hides all git affordances, so users of non-repo directories see exactly no change.

Everything is read-only: no checkout, commit, stage, fetch, push, or any other write operation exists. The implementation uses go-git, so the binary still has no runtime dependency on the git CLI.

Security model

explorer is designed for trusted, local use:

  • Read-only. No write, update, or delete endpoints exist. The binary never modifies the served tree.
  • Loopback by default. --host defaults to 127.0.0.1, so out of the box only your own machine can reach it.
  • Path-traversal containment. Every path-accepting endpoint resolves the request relative to the served root, rejects .. and absolute paths, and follows symlinks only if the resolved target stays inside the root. Symlinks pointing outside the root are silently dropped from listings and return 400 outside_root when accessed directly.
  • No authentication. There is no login, no token, no per-file ACL.

Because there is no authentication, --host 0.0.0.0 exposes every file under the served root to anyone who can reach your machine on the network. Only do this on a network you trust. Do not expose explorer to the public internet.

Development

Two-process dev workflow: Go server on :8080 + Vite dev server on :5173 proxying API/raw paths to the Go server.

make dev-server       # builds and runs the Go binary against ./testdata
make dev-web          # starts the Vite dev server on :5173 (run alongside dev-server)
make test             # go test ./cmd/... ./internal/...
make web-commit       # rebuild the embedded SPA and stage internal/server/ui/dist/
make clean            # remove ./explorer and internal/server/ui/dist

# Git-reader capability
make sample-repo      # (re)generate ./testdata/sample-repo with deterministic commits
make dev-git-server   # run the Go binary against the sample git repo (depends on sample-repo)
make e2e              # build + sample-repo + run Playwright e2e suite (chromium)
make e2e-deps         # one-time: install playwright browser system libs (needs sudo on linux)

End-to-end tests

The make e2e target builds the binary, regenerates the sample-repo fixture (deterministic SHAs), and runs Playwright against two instances of the binary — one pointed at the sample repo, one at a non-repo directory — to exercise the git surface end to end. On a fresh machine run make e2e-deps once to install the browser system libraries (this uses sudo). The Playwright workspace lives under e2e/ and is independent of the SPA's package.json so SPA builds don't pull Playwright as a transitive dev dep.

internal/server/ui/dist/ is committed to source control so module-proxy installs (go install, go run) ship a working SPA. Run make web-commit whenever you change anything under web/src/ so the embedded build stays in sync.

Out-of-scope (v1)

These are intentionally deferred:

  • File watcher / live reload
  • Search across files
  • Syntax highlighting for code ✓ shipped — Go, TypeScript, Python, Kotlin, Swift, Rust, and 35+ more languages plus JSON/TOML/GraphQL/Protobuf; see web/src/components/syntax/grammars.ts for the full list
  • README auto-render in folder listings
  • Tree pagination per folder
  • Authentication / multi-user
  • Write / update / delete
  • PDF, Office, video, audio preview

Troubleshooting

"connection refused" on port 8080 in Docker

Make sure the command in docker-compose.yml includes --host 0.0.0.0. The default loopback bind is not reachable through Docker's port mapping.

GET / returns a plain-text 404 (404 page not found) after a local build

The embedded SPA is empty. Run make build (not just go build) to build the frontend and embed it. If you got the binary via go install ...@latest and still see this, the published internal/server/ui/dist/ is empty — please open an issue, this should not happen.

Symlink file is missing from listing

Symlinks whose targets resolve outside the served root are silently omitted from directory listings and return a 400 outside_root error when accessed directly — this is intentional path-traversal containment.

Contributing

Issues and pull requests are welcome.

  • Run make test before submitting a PR.
  • If your PR touches web/src/, run make web-commit and include the regenerated internal/server/ui/dist/ in the same commit. The embedded SPA is shipped via the Go module proxy, so a stale dist/ means go install users get an outdated frontend against an updated API.

License

MIT © anantadwi13

About

A local file explorer. Preview markdown, read code with syntax highlighting, and inspect git history & blame - all from one binary.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors