{"id":50491298,"url":"https://github.com/pzzzy/macftpd","last_synced_at":"2026-06-02T03:03:51.809Z","repository":{"id":360854235,"uuid":"1248056648","full_name":"pzzzy/macftpd","owner":"pzzzy","description":"Go FTP server with HTTP admin/public interface for macOS file-host workflows","archived":false,"fork":false,"pushed_at":"2026-05-28T06:18:17.000Z","size":113,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T07:10:52.497Z","etag":null,"topics":["cloudflare","ftp-server","go","http-api","launchd","macos","networking","portfolio"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pzzzy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-24T06:04:56.000Z","updated_at":"2026-05-28T06:18:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pzzzy/macftpd","commit_stats":null,"previous_names":["pzzzy/macftpd"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/pzzzy/macftpd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pzzzy%2Fmacftpd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pzzzy%2Fmacftpd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pzzzy%2Fmacftpd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pzzzy%2Fmacftpd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pzzzy","download_url":"https://codeload.github.com/pzzzy/macftpd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pzzzy%2Fmacftpd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33803735,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","ftp-server","go","http-api","launchd","macos","networking","portfolio"],"created_at":"2026-06-02T03:03:50.887Z","updated_at":"2026-06-02T03:03:51.802Z","avatar_url":"https://github.com/pzzzy.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# macftpd\n\n`macftpd` is a Go-powered FTP server with a companion HTTP admin/public interface for a modern macOS file host.\n\nCurrent capabilities:\n\n- FTP control/data server with USER/PASS, passive EPSV/PASV, active PORT/EPRT, LIST/NLST, upload, download, delete, mkdir, rename, SIZE, and MDTM.\n- Explicit FTPS with `AUTH TLS`, `PBSZ`, `PROT`, plus modern `MLSD`/`MLST` listings.\n- User and group permissions for list, download, upload, delete, mkdir, rename, admin, public, and dropbox workflows.\n- Storage-root containment, default macOS/security ignore rules, and virtual `public`/`dropbox` mounts for permitted users.\n- HTMX + Tailwind CSS + daisyUI HTTP admin UI and JSON API for user management, file listing/detail/download/chunked-upload/rename/delete, copy/move, public share links, upload drop links, link revocation, live session status, trash/version restore, Cloudflare purge, and remote FTP pull into local storage.\n- Public HTTP file serving from the configured `public` folder with sortable directory listings, cache headers, optional Cloudflare cache tags, and download/referrer analytics.\n- Short direct share URLs (`/s/\u003cid\u003e/\u003ctoken\u003e/\u003cfilename\u003e`) that serve bare files with correct MIME and `Content-Disposition` behavior; image/video/PDF/text content opens inline, archive-style content downloads.\n- Password-protected share/drop links with secure share-scoped cookies, one-download links, timed expiry, never-expiring links, and admin-visible persistent link URLs.\n- Public upload drops (`/d/\u003cid\u003e/\u003ctoken\u003e`) with the same chunked upload path as the admin UI; uploads into `public` return the public download URL.\n- NAT-PMP and UPnP IGD automatic TCP port mapping for FTP control and passive data ports when the router supports it.\n- Remote macOS deployment through launchd with a repairable `/opt/macftpd` app folder and `/srv/macftpd/files` storage root.\n\n## Local Development\n\n```bash\nnpm install\nnpm run build\ngo test ./...\ngo run ./cmd/macftpd -config configs/macftpd.example.json\n```\n\nThe generated admin/public CSS and local HTMX asset are embedded into the Go binary from `internal/httpapi/static`. Run `npm run build` after changing templates or CSS.\n\nFor local-only testing, override paths and ports:\n\n```bash\nMACFTPD_STORAGE_ROOT=\"$PWD/var/ftpd\" \\\nMACFTPD_USERS_PATH=\"$PWD/var/users.json\" \\\nMACFTPD_ADMIN_PASS=\"secret\" \\\nMACFTPD_FTP_LISTEN=\"127.0.0.1:2121\" \\\nMACFTPD_HTTP_LISTEN=\"127.0.0.1:8080\" \\\ngo run ./cmd/macftpd\n```\n\nAdmin UI:\n\n```text\nhttp://127.0.0.1:8080/admin\n```\n\nUse HTTP Basic auth or `POST /api/login` with the admin credentials.\n\n## Remote macOS Deploy\n\nThe deploy script builds a Darwin/arm64 binary locally, copies it to a remote Mac, installs a LaunchAgent, and starts/restarts the service. Set `REMOTE` to your SSH host. If you do not use an SSH agent, also set `KEY` to your private key path.\n\n```bash\nREMOTE='macftpd@example-host.local' KEY='/path/to/ssh-key' \\\nADMIN_PASS='choose-a-strong-password' ./scripts/deploy-remote-macos.sh\n```\n\nThe first deploy writes `/opt/macftpd/config.json`. Later deploys merge generated operational settings into the active config while preserving secrets such as the admin password and session key. The previous active config is backed up as `config.json.backup.\u003ctimestamp\u003e`, and the generated config is also kept as `config.json.last_deployed`.\n\nOverride `REMOTE_DIR` and `STORAGE_ROOT` for site-specific installs, for example a home-directory app folder with an external-volume FTP root:\n\n```bash\nREMOTE='macftpd@example-host.local' KEY='/path/to/ssh-key' \\\nREMOTE_DIR='~/macftpd' STORAGE_ROOT='/path/to/ftpd-storage' \\\nADMIN_PASS='choose-a-strong-password' ./scripts/deploy-remote-macos.sh\n```\n\nBy default the deploy starts the service in `START_MODE=manual`, launched over SSH. This is useful while validating macOS privacy permissions for external or removable storage. After granting the binary external volume or Full Disk Access, use:\n\n```bash\nSTART_MODE=launchd ADMIN_PASS='choose-a-strong-password' ./scripts/deploy-remote-macos.sh\n```\n\nSmoke test against remote Mac:\n\n```bash\nADMIN_PASS='same-password' ./scripts/smoke-remote.sh\nADMIN_PASS='same-password' HOST=192.0.2.10 ./scripts/protocol-lab.sh\n```\n\n## Cloudflare HTTP Front Door\n\n`https://ftp.example.com` is served through Cloudflare Tunnel and a Worker:\n\n- `macftpd-tunnel` tunnel forwards `ftp.example.com` and `macftpd-origin.example.com` to `http://127.0.0.1:8080` from the remote Mac connector, so LAN IP changes do not break the HTTP front door.\n- `macftpd-public-cache` Worker runs on `ftp.example.com/*`.\n- `/public/*` responses are cached with Cloudflare Cache API and include `X-Macftpd-Cache: MISS` or `HIT`.\n- `/admin/`, `/api/*`, and health checks are proxied with `Cache-Control: no-store`.\n\nStart or repair the remote Mac tunnel connector:\n\n```bash\nTUNNEL_TOKEN_FILE=/path/to/token ./scripts/start-cloudflare-tunnel.sh\n```\n\nThe token is stored on the remote Mac at `/opt/macftpd/var/cloudflared.env.token` with mode `0600`, and the screen session is `macftpd-cloudflared`.\n\nDeploy or repair the Worker route:\n\n```bash\nwrangler deploy --config cloudflare/wrangler.jsonc\n```\n\nThe Worker forwards the public host to the origin with `X-Forwarded-Host` and `X-Macftpd-Public-Host` so admin CSRF checks see browser requests from `ftp.example.com`, while `/public/*` remains cached at the edge and admin/API traffic remains `no-store`.\n\nOptional hardening helpers:\n\n```bash\nCF_ZONE_ID=... CF_API_TOKEN=... ./scripts/cloudflare-hardening.sh\nCF_ACCOUNT_ID=... CF_API_TOKEN=... ALLOW_EMAILS='admin@example.com' ./scripts/cloudflare-access-admin.sh\n```\n\n`cloudflare-hardening.sh` maintains a zone WAF ruleset for the public hostname. `cloudflare-access-admin.sh` creates or updates a Cloudflare Access self-hosted application for `/admin*`; keep the built-in macftpd admin login enabled as a second layer.\n\n## Shares, Drops, And Public Analytics\n\nAdmins can create and revoke links from the `/admin` Links panel or the `/api/shares` endpoint. New links persist their token-bearing URL path in `var/shares.json` so the admin list can continue showing useful URLs after refresh. Existing legacy links that were created before URL persistence may need to be revoked and recreated if their full token URL is no longer known.\n\nDownload shares use short `/s/\u003cid\u003e/\u003ctoken\u003e` URLs. For file shares, macftpd appends the original filename to the returned URL for readability without leaking the storage path. The share handler still authorizes by id/token, not by the display filename. Shared files are served directly: images, videos, audio, PDFs, and text are inline by default, while other file types use attachment disposition. Add `?download=1` to force attachment behavior.\n\nUpload drops use `/d/\u003cid\u003e/\u003ctoken\u003e` URLs. Protected drops first accept a password form, then set a secure, HttpOnly, share-scoped cookie before showing the compact chunked upload UI. Drops created against the public folder return a `public_url` after upload, such as `/public/example.mp4`.\n\nPublic and shared downloads are recorded in the activity log with count, last download time, remote address, byte count, and HTTP referrer. Admin file detail cards summarize these stats through `/api/stats?path=\u003cstorage-path\u003e`.\n\nExpiry presets in the admin UI are `1 download`, `1h`, `12h`, `24h`, `1w`, `1m`, and `never`.\n\n## HTTP API Highlights\n\nAll `/api/*` endpoints require an admin session or HTTP Basic auth unless noted. Unsafe methods enforce same-origin checks using `Origin`, Fetch Metadata, and Cloudflare forwarded-host headers.\n\n```bash\nauth=(-u \"$MACFTPD_ADMIN_USER:$MACFTPD_ADMIN_PASS\")\n\n# Create a direct download share.\ncurl \"${auth[@]}\" -H 'content-type: application/json' \\\n  -d '{\"kind\":\"download\",\"path\":\"/public/example.mp4\",\"expires_in\":\"24h\"}' \\\n  https://ftp.example.com/api/shares\n\n# Create a password-protected public upload drop.\ncurl \"${auth[@]}\" -H 'content-type: application/json' \\\n  -d '{\"kind\":\"upload\",\"path\":\"/public\",\"expires_in\":\"1h\",\"password\":\"optional\"}' \\\n  https://ftp.example.com/api/shares\n\n# List and revoke links.\ncurl \"${auth[@]}\" https://ftp.example.com/api/shares\ncurl \"${auth[@]}\" -X DELETE https://ftp.example.com/api/shares/\u003cid\u003e\n\n# Inspect public/share download stats for a storage path.\ncurl \"${auth[@]}\" 'https://ftp.example.com/api/stats?path=/public/example.mp4'\n```\n\nOther admin endpoints include `/api/users`, `/api/groups`, `/api/files`, `/api/files/action`, `/api/upload/chunk`, `/api/download`, `/api/fxp`, `/api/activity`, `/api/status`, `/api/doctor`, `/api/retention`, `/api/retention/restore`, and `/api/cloudflare/purge`.\n\n## Release Gate\n\nBefore a release candidate, run:\n\n```bash\ngo test ./...\ngo test -race ./...\ngo vet ./...\nnpm run build\n./scripts/check-private-identifiers.sh\ngo run github.com/securego/gosec/v2/cmd/gosec@latest ./...\ngo run golang.org/x/vuln/cmd/govulncheck@latest ./...\nwrangler deploy --config cloudflare/wrangler.jsonc --dry-run\nADMIN_PASS=\"$(cat var/admin-pass.txt)\" ./scripts/smoke-remote.sh\n```\n\nThen verify through `https://ftp.example.com`: admin login, create/edit/delete a test user, chunked admin upload of a large file, direct `/s/` share of that file with correct MIME/disposition, protected `/d/` drop upload, public cache `MISS` then `HIT`, and the remote Mac monitor screen showing `status=ok`.\n\n## Network Notes\n\nFor internet exposure, macftpd can automatically map these with NAT-PMP and UPnP IGD:\n\n- TCP `2121` to the remote Mac for FTP control.\n- TCP `50000-50100` to the remote Mac for passive FTP data.\n- TCP `8080` or a reverse-proxied HTTPS endpoint for HTTP/admin/public.\n\nUse `\"external_ip\": \"auto\"` with `\"auto_map\": true` to advertise the discovered public address in classic PASV replies. Passive FTP data ports are mapped on demand and released after the data connection is closed or the passive setup is abandoned. macftpd tries NAT-PMP and UPnP IGD when available. Set `ftp.external_ip` to a fixed public IP or DNS target if the router does not support automatic mapping. EPSV-capable clients usually work better through NAT.\n\nDefault storage ignore rules hide and deny downloads for macOS metadata and sensitive dot-directories such as `.DS_Store`, `._*`, `.AppleDouble`, `.Spotlight-V100`, `.Trashes`, `.git`, `.env`, and `.ssh`. Adjust `storage.ignore` in `config.json` if you need a different policy.\n\nDeletes move files into `._macftpd_trash`, and overwrites create retained versions under `._macftpd_versions`; both locations are hidden by default ignore rules. Restore from the admin UI or `/api/retention/restore`.\n\nKeep `ftp.allow_fxp` disabled unless you explicitly trust server-to-server active FTP targets. The HTTP `/api/fxp` endpoint performs authenticated remote FTP pulls into local storage and is admin-only.\n\n## FTPS Certificates\n\nmacftpd supports optional explicit FTPS when `ftp.tls_cert_file` and `ftp.tls_key_file` are configured. A free Let's Encrypt certificate can be renewed with:\n\n```bash\nMACFTPD_APP_DIR=/opt/macftpd \\\nMACFTPD_ACME_DOMAIN=ftp.example.com \\\n/opt/macftpd/bin/renew-ftps-cert.sh\n```\n\nThe renewal helper uses Certbot's `renew` flow when a lineage already exists, serves HTTP-01 challenge files from `/public/.well-known/acme-challenge/`, installs renewed certs through a deploy hook, and restarts the running macftpd process so the new certificate is presented. The example LaunchAgent `launchd/com.example.macftpd.cert-renew.plist` runs renewal checks twice daily with jitter.\n\n## macOS File Access\n\nThe launchd service runs as the target login user, so macOS privacy and external-volume permissions apply to that user. If the service cannot see `/srv/macftpd/files`, grant the hosting terminal/app Full Disk Access or run the first launch interactively once from Terminal:\n\n```bash\n/opt/macftpd/bin/macftpd -config /opt/macftpd/config.json\n```\n\n## Repair\n\n```bash\nssh macftpd@example-host.local 'launchctl kickstart -k gui/$(id -u)/com.example.macftpd'\nssh macftpd@example-host.local 'tail -100 /opt/macftpd/var/macftpd.err.log'\n```\n\nRe-run `./scripts/deploy-remote-macos.sh` to replace the binary and reinstall launchd without deleting user data or FTP storage.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpzzzy%2Fmacftpd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpzzzy%2Fmacftpd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpzzzy%2Fmacftpd/lists"}