A single Go binary that lets anyone drop files on me, and only me open the box they land in.
Pingvin Share is me to others. This is the opposite: others to me. I generate a link from an admin dashboard, send it to someone, and they drag files, or a whole folder, onto a plain page. No account, no install, no login.
I built it rather than reusing something because every inbound file-drop project I found bundled a full account system I didn't need for a single admin. It's one Go binary with the React frontend embedded via go:embed. No separate frontend container, no external database.
Both the admin dashboard and every generated drop link are served by one container on port 8080. Caddy fronts it with two labels: a public host on Let's Encrypt, an internal host on Caddy's own CA.
One admin password, and optionally one per link. Both hash the same way: PBKDF2-HMAC-SHA256 with a random 16-byte salt and 210,000 iterations, compared in constant time so a slow comparison can't leak the answer one byte at a time.
const (
pbkdf2Iterations = 210_000
pbkdf2KeyLen = 32
saltLen = 16
)
func hashPassword(password string, salt []byte) []byte {
return pbkdf2.Key([]byte(password), salt, pbkdf2Iterations, pbkdf2KeyLen, sha256.New)
}
candidate := hashPassword(password, salt)
if subtle.ConstantTimeCompare(candidate, hash) != 1 {
return "", ErrWrongPassword
}
Plain SHA-256 is fast, which is exactly wrong for a password hash, since it lets anyone with a stolen database try billions of guesses a second. PBKDF2 re-runs it as an HMAC 210,000 times to make each guess expensive. The salt means two identical passwords never produce the same stored hash, which kills precomputed rainbow tables.
A successful login doesn't hand back a JWT. It mints 32 random bytes, hex-encodes them, and stores that token server-side with a 30-day expiry. The browser only ever sees the token, as an HttpOnly, Secure, SameSite=Lax cookie. Logging out deletes the row. Attempts are rate-limited to five per minute per IP.
12-character token · expiry · file and size caps · optional password
GET /info returns title, active, remaining. Never the hash.
The WebKit entries API recurses folders, keeping relative paths intact
XMLHttpRequest, not fetch. Only XHR reports progress. Three at a time.
Destination clamped inside the link's own directory, so traversal is refused
MIME sniffed from the first 512 bytes · byte budget enforced mid-stream
Re-check limits against live counts, then INSERT
Goroutine renders a 400 px JPEG via image/draw, ffmpeg or pdftoppm
Checking the limits before writing isn't enough. Three files landing at once each read the same pre-insert count, each see room under the cap, and all three commit, quietly blowing past a limit that was supposed to be hard. Classic time-of-check to time-of-use.