dropgallery
v0.1.1
Published
CLI that turns a selection of images and videos into a single shareable static gallery hosted on S3 + CloudFront. macOS-first (Apple Shortcut → native dialogs) with a cross-platform terminal mode.
Maintainers
Readme
DropGallery
Turns a selection of images and videos into a shareable static gallery hosted on S3 + CloudFront. Built for macOS (native dialogs, Finder integration using Shortcuts), with a terminal mode for Linux and Windows.

Motivation
There are countless tools for making photo galleries out there, and this is one of them! Why not use others?
Because this one does what I really, really want.
"So tell me what you want, what you really, really want"
- Hosted on your own S3 bucket + CloudFront distribution
- Uses your own domain (e.g.
gallery.mydomain.com) with TLS - Produces statically-generated galleries (no server-side processing)
- Generates random, unguessable URLs (easily shared, but still private)
- Optional expiration (1, 7, 30 days, or never)
- Supports images and videos
- Minimal design - no logins, tracking, analytics, or other fluff
- Easy to integrate into macOS Finder (and still usable on other platforms)
What it does
files → prompts (confirm / title / expiration)
→ sharp + ffmpeg generate thumbnails
→ render dark-themed PhotoSwipe gallery
→ upload to S3 with expire-days tags
→ CloudFront URL → clipboard- 1, 7, 30-day, or never expirations (S3 lifecycle rules do the actual deletion)
- Dark-themed responsive grid with a click-to-expand lightbox (PhotoSwipe v5)
- HEIC/HEIF/AVIF auto-converted to JPEG so non-Safari browsers display them
- Videos get a real poster frame if
ffmpegis on PATH, a placeholder otherwise - One unguessable URL per gallery (128-bit base64url ID); URLs are the only access control
- macOS: native dialogs + a Notification Center toast when work begins. Linux / Windows: terminal prompts + spinner
Prerequisites
- Node.js ≥ 20.17 (current Node 20 LTS) on macOS, Linux, or Windows
- AWS account + a way to attach a custom TLS domain
ffmpeg(optional but recommended for video posters):- macOS:
brew install ffmpeg - Linux:
apt install ffmpeg/dnf install ffmpeg - Windows: https://ffmpeg.org/download.html
- macOS:
- macOS only: uses built-in
osascript,pbcopy,open— no install needed
One-time AWS setup
1. S3 bucket
- Create a new S3 bucket dedicated to this tool.
- Block all public access (default).
2. CloudFront distribution
- Create a CloudFront distribution.
- Origin: the S3 bucket above. Use OAC (Origin Access Control) so the bucket stays private and only CloudFront can read it.
- Custom TLS domain (e.g.
gallery.yourdomain.com): attach an ACM certificate issued in us-east-1 (CloudFront only reads us-east-1 certs). - Error responses: configure 404 (and optionally 403) however you like. Don't enable directory listing — that would make
/g/enumerable.
No CloudFront Functions or Lambda@Edge needed; each gallery is plain static files. The HTML lands at the bare key g/<id> with Content-Type: text/html, and a <base href="<id>/"> makes its relative asset paths resolve correctly.
CloudFront propagation takes a few minutes globally; URLs may 404 until then.
3. Lifecycle rules (the expiration mechanism)
On the bucket, create three lifecycle rules. Each is filtered by exactly one tag value and expires matching objects:
| Rule name | Tag filter | Expire current versions after |
| --------------- | ---------------- | ----------------------------- |
| expire-1day | expire-days=1 | 1 day |
| expire-7days | expire-days=7 | 7 days |
| expire-30days | expire-days=30 | 30 days |
Do not create a bucket-wide catch-all rule — galleries with the "Never" expiration carry no tag, and a catch-all would defeat that.
4. IAM credentials
Create an IAM user (or role) with this minimal policy on the bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectTagging"],
"Resource": "arn:aws:s3:::your-gallery-bucket/*"
}
]
}Add the access keys to ~/.aws/credentials under a named profile:
[gallery]
aws_access_key_id = AKIA...
aws_secret_access_key = ...The script never reads raw access keys — it loads the profile via the AWS SDK.
Install
Once the AWS prerequisites above are in place:
npm install -g dropgallery
dropgallery --setupdropgallery --setup walks you through the four config values (profile, region, bucket name, CloudFront domain), validates them against AWS by uploading a tagged test object, and writes the config to the OS-standard application config directory:
- macOS:
~/Library/Preferences/dropgallery/galleryrc - Linux:
~/.config/dropgallery/galleryrc - Windows:
%APPDATA%\dropgallery\Config\galleryrc
If you prefer a dotfile in your home directory, ~/.galleryrc is checked first and wins if it exists. The format is INI, with one [section] per destination:
[default]
AWS_PROFILE=gallery
AWS_REGION=us-east-1
S3_BUCKET=your-gallery-bucket
CLOUDFRONT_DOMAIN=gallery.yourdomain.comYou can re-run dropgallery --setup any time to regenerate the config. Old flat-format configs (no [section] headers) from earlier versions still work — they're treated as a single destination named default.
Multiple destinations
If you wanna upload to more than one bucket / domain — e.g. one for personal galleries and one for work — add additional sections:
[personal]
AWS_PROFILE=gallery
AWS_REGION=us-east-1
S3_BUCKET=personal-gallery-bucket
CLOUDFRONT_DOMAIN=gallery.mydomain.com
[work]
AWS_PROFILE=work-gallery
AWS_REGION=us-west-2
S3_BUCKET=work-gallery-bucket
CLOUDFRONT_DOMAIN=gallery.work.example.comEach section is independent — its own AWS profile, region, bucket, and CloudFront domain. Each bucket needs the same one-time setup (lifecycle rules, IAM policy, CloudFront distribution).
To add a new destination via the wizard:
dropgallery --setup --as workThis adds (or replaces) just the [work] section, leaving any other destinations untouched.
With one destination configured, DropGallery uses it automatically. With more than one, you're prompted — or skip the prompt with --target <name> or DROPGALLERY_TARGET=<name>:
dropgallery --target work path/to/photo.jpgInstalling from source (development)
git clone https://github.com/turley/dropgallery.git
cd dropgallery
npm install
node gallery.js --setup # or: npm link && dropgallery --setup
npm test # smoke tests, no AWS requiredApple Shortcut (macOS)
A small wrapper that hands the selected Finder files (or folders) to the dropgallery command.
Install
Open Make Gallery (iCloud Shortcut) on macOS 12 or later, then tap Add Shortcut. You may need to manually allow the shortcut to access the shell action. That's it.
The shortcut is a single Run Shell Script action:
export PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node 2>/dev/null | tail -1)/bin:$PATH"
dropgallery "$@"The PATH line covers the three places npm install -g typically puts the bin (Apple Silicon Homebrew, Intel Homebrew / system Node, nvm). If which dropgallery shows yours somewhere else, edit the shortcut after installing — or build it yourself.
- Open Shortcuts.app → File → New Shortcut (or click
+). - Name it Make Gallery (or whatever you want).
- Click the small ⓘ ("info") button in the editor's toolbar to reveal the Shortcut Details panel on the right.
- In the Details panel:
- Toggle on Use as Quick Action.
- Check Finder.
- Set Receive to Files, Folders (so the action shows up for both individual files and folders selected in Finder).
- macOS auto-inserts a Receive [Files and Folders] from Quick Actions step at the top of the workflow. Leave it.
- Drag a Run Shell Script action below it. Configure:
- Shell:
zsh - Pass Input: as arguments
- Script: the two lines shown above.
- Input: Shortcut Input with Pass input as arguments.
- Shell:
- Save (
⌘S).
Use it
Select files or a folder in Finder, right-click → Quick Actions → Make Gallery (it may live under a "More…" submenu if you have a lot of Quick Actions). Folders are walked recursively for supported extensions (.jpg/.jpeg/.png/.webp/.gif/.heic/.heif/.avif/.mp4/.mov); a mix of files and folders is fine.

Optional: keyboard shortcut
In Shortcuts.app, select Make Gallery, open the ⓘ Details panel, and click Add Keyboard Shortcut — assign something like ⌥⌘G.
Usage from terminal
dropgallery path/to/a.jpg path/to/b.heic path/to/c.mp4
dropgallery path/to/folder # walks recursively
dropgallery folder1 folder2 some-loose-photo.jpg # mix is fine
dropgallery --help # full flag listOn macOS, this runs the same native dialogs as the Apple Shortcut. To force the terminal UI instead (e.g. for unattended scripts), pass --cli:
dropgallery --cli path/to/a.jpg path/to/b.heicLinux and Windows always use the terminal UI. DROPGALLERY_UI=cli is equivalent to --cli on macOS.
With multiple destinations configured, --target <name> (or DROPGALLERY_TARGET=<name>) skips the destination prompt:
dropgallery --target work path/to/a.jpgLogs
Every run writes a timestamped .log file in the OS-appropriate log directory:
- macOS:
~/Library/Logs/dropgallery/ - Linux:
~/.local/share/dropgallery/log/ - Windows:
%LOCALAPPDATA%\dropgallery\Log\
Successful runs are short; failures include full stack traces and any captured ffmpeg / AWS SDK output. Logs aren't rotated — clean them out periodically if you care.
Troubleshooting
- "AWS auth failure" / SignatureDoesNotMatch — check the
AWS_PROFILEin the relevant[section]of your config (~/Library/Preferences/dropgallery/galleryrcon macOS) matches a section in~/.aws/credentials, and that the IAM policy grantss3:PutObjectands3:PutObjectTaggingon the bucket. Re-rundropgallery --setup --as <name>to validate. Destination "X" not found— edit the relevant[X]section of your config file, or rundropgallery --setup --as Xto create it.- Gallery URL returns 404 — CloudFront takes 5–15 minutes to deploy after first creation. After that, check OAC is correctly attached and the bucket policy grants read to the CloudFront principal. Verify the gallery's HTML object exists in the S3 console at the bare key
g/<id>(no extension). - Object not expiring — check the lifecycle rule's tag filter is
expire-days=N(no spaces); inspect actual tags viaaws s3api get-object-tagging --bucket your-gallery-bucket --key g/<id>(the HTML) or--key g/<id>/images/<file>(an asset). - Videos show placeholder instead of poster frame — ffmpeg isn't on
$PATHin the Shortcut's minimal environment. Install it into/usr/local/binor/opt/homebrew/bin, or setPATHexplicitly in the shell-script action. - HEIC files fail to convert — sharp's prebuilt binaries should bundle libheif on macOS. If conversion fails, run
npm rebuild sharpornpm install sharp@latest. - (Linux)
clipboardyerrors with "Cannot find xclip / wl-copy" — install one:apt install xclip(X11) orapt install wl-clipboard(Wayland). - (Linux)
Sharp install failed— older glibc / Alpine setups may neednpm install --include=optional sharpor system libvips. See sharp's install docs.
Out of scope (intentionally)
- Password-protected galleries / signed URLs — URL unguessability is the only access control
- Analytics, upload UI, albums, EXIF display, sort by date
- Automatic log rotation
License
MIT — see LICENSE.
PhotoSwipe v5 is vendored in templates/photoswipe/ and retains its own MIT license — see templates/photoswipe/LICENSE.
