A single 16MB Go binary that serves a Finder-style browser UI for EC2 file management and S3 browsing. Connect via SSH, drag-and-drop upload files, edit with Monaco, and browse S3 buckets — no SCP commands, no tab-switching.
Solo Engineer (full stack)
Managing EC2 files traditionally means juggling scp, ssh, and vim across terminal tabs. Existing GUI tools (Cyberduck, Transmit) require separate installs, licensing, or lack S3 integration. The challenge was shipping a zero-install GUI — one binary the user drops in $PATH — that handles SSH auth, live SFTP operations, S3 browsing, and serves a full React frontend, all without Node.js or a separate web server.
Single 16MB Go Binary: Embedded React + SSH/SFTP + S3 + Monaco
$ ape [--host ec2-user@54.x.x.x] [--key ~/.ssh/id_rsa]
│
├─ Parse CLI flags (host, key, port, bind-addr)
├─ sshClient.Dial("tcp", host+":22") → ssh.NewSession()
│ └─ sftp.NewClient(sshConn) → SFTP session
├─ s3Client = aws.NewConfig() → s3.NewFromConfig()
│
├─ Embed React build via //go:embed dist/*
│ └─ http.FileServer(http.FS(embedFS)) → /
│
├─ Register API routes:
│ ├─ GET /api/files?path= → sftp.ReadDir()
│ ├─ GET /api/file?path= → sftp.Open() → stream
│ ├─ POST /api/upload → multipart → sftp.Create()
│ ├─ POST /api/mkdir → sftp.MkdirAll()
│ ├─ DEL /api/delete → sftp.Remove()
│ ├─ GET /api/s3/buckets → s3.ListBuckets()
│ └─ GET /api/s3/objects → s3.ListObjectsV2()
│
└─ http.ListenAndServe(bindAddr, mux)
└─ Web UI ready at http://localhost:9000POST /api/upload [multipart/form-data: file, path]
│
├─ r.ParseMultipartForm(32 MB)
├─ file, header = r.FormFile("file")
├─ remotePath = r.FormValue("path") + "/" + header.Filename
│
├─ sftpClient.Create(remotePath) ← opens remote file for writing
│ └─ If parent dir missing → sftpClient.MkdirAll(dir)
│
├─ io.Copy(remoteFile, file) ← streams bytes, no local temp
│ └─ Progress tracked via custom io.Writer wrapper
│ └─ SSE stream → client EventSource (drag & drop bar)
│
└─ res.JSON({ success: true, path: remotePath, size: header.Size })FileExplorer (React 18 + TypeScript)
│
├─ State: { path, entries, view: "grid"|"list", selected: Set<string> }
│
├─ Grid View: thumbnail cards with file-type icons (Monaco-derived)
├─ List View: sortable table (name / size / modified)
│
├─ Right-click context menu:
│ ├─ Open in Monaco Editor → GET /api/file → editor modal
│ ├─ Download → anchor[download] blob URL
│ ├─ Rename → POST /api/rename
│ └─ Delete → DEL /api/delete (confirm dialog)
│
├─ Drag & Drop zone:
│ └─ onDrop → FormData → POST /api/upload → SSE progress bar
│
├─ Keyboard shortcuts:
│ ├─ Cmd+N → new folder dialog
│ ├─ Delete → delete selected
│ └─ Cmd+C → copy path to clipboard
│
└─ Breadcrumb nav: click any segment → navigate to that path//go:embed dist/*. No separate web server, no Node.js runtime — the user runs one binary and gets the full UI.io.Copy(remoteFile, file) streams upload bytes directly to the EC2 filesystem without writing a local temp file, keeping memory footprint flat regardless of file size.aws s3 ls. Credentials read from ~/.aws/credentials or env vars — same auth model users already have.CI via GitHub Actions on every push; Go build matrix (linux/amd64, darwin/arm64, darwin/amd64, windows/amd64).
Manual integration tests against a live EC2 t2.micro and real S3 buckets. Edge cases exercised across OS-specific SSH key formats (PEM, OpenSSH new format).
{ error: string } JSON on failure — the React UI surfaces these as toast notifications.errors.As(err, &apiErr) to surface the AWS error code to the user.scp/ssh/vim workflow for EC2 file management — drag, drop, edit, done.brew install or direct GitHub Release download.