A Node.js CLI tool to manage a photography portfolio. It uploads photos from local folders, extracts EXIF metadata, uploads images to Cloudinary, stores metadata in Firebase Firestore, and manages featured photos. This CLI is designed to be safe, repeatable, and idempotent. Duplicate photos are automatically skipped using file hashing.
- 📤 Batch upload photos from multiple folders
- 🔁 Skip duplicate images using SHA-1 hash
- 🏷 Auto-rename images to sequential IMG-XXXX
- 📷 Extract EXIF metadata (camera, ISO, aperture, shutter, shot date)
- ☁ Upload images to Cloudinary
- 🔥 Store metadata in Firebase Firestore
- ⭐ Mark photos as featured
- 🧹 Clear all featured photos
- 💥 Reset database with double confirmation
- 📊 CLI progress bar
- ⚠ Strong error handling with hard failures
- Node.js + TypeScript
- Firebase Firestore (Admin SDK)
- Cloudinary
- exifr for EXIF parsing
- prompt-sync for CLI interaction
project-root/
├── photos-cli.ts # Main CLI entry point
├── firebase-admin.ts
├── .env
├── package.json
└── README.md
Run:
npm install
Save the private key from Firebase consolve in root/credentials/firebase-admin.json
Then rename it firebase-admin.json
Create a .env file:
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Firebase Admin credentials should be configured inside firebase-admin.ts.
The upload command expects a text file that lists photo folders. Example folders.txt
/Users/me/photos/2023/trip
/Users/me/photos/2024/portraits
- Each line is a folder path
- All images inside each folder are scanned
- Supported formats: jpg, jpeg, png, webp
- All folders are scanned recursively (non-recursive per folder)
- Images are sorted alphabetically
- The next available IMG-XXXX number is detected from Firestore
- Each file:
- Is hashed (SHA-1)
- Skipped if hash already exists in database
- Uploaded to Cloudinary
- Stored in Firestore with metadata If the database is empty, numbering starts at:
IMG-0000
Collection: photos
{
title: string // IMG-XXXX
imageUrl: string
width: number
height: number
camera: string | null
aperture: string | null
shutterSpeed: string | null
iso: number | null
shotDate: Date | null
hash: string // SHA-1
featured: boolean
}
Run the CLI:
npx ts-node src/photos-cli.ts
You will see:
What do you want to do?
[1] Upload photos
[2] Set featured photos
[3] Remove all featured photos
[4] Reset database (DANGEROUS)
[0] Exit
Choose option [1] Upload photos You will be prompted for the path to the text file:
Enter path to text file with photo folders:
The CLI will:
- Upload in batches
- Show a progress bar
- Fail hard if any photo errors occur
Choose option [2] Set featured photos Input image numbers only:
Enter image numbers to feature (example: 5001,5003,5010): This will mark:
IMG-5001
IMG-5003
IMG-5010
as featured: true.
Choose option [3] Remove all featured photos This resets all featured flags to false.
Choose option [4] Reset database Requires double confirmation:
Type RESET to continue:
Type DELETE ALL to confirm:
This permanently deletes all photo documents from Firestore.
- Duplicate photos are skipped safely
- Upload failures stop the process
- Errors throw and exit with non-zero status
- Progress bar updates on a single line
- Always upload from raw folders
- Never manually edit IMG-XXXX
- Use featured flags for homepage selection
- Backup Firestore before resetting
- The CLI is intentionally dependency-light
- prompt-sync is used instead of inquirer for reliability
- Designed for local + CI environments