Files
theread.me/photo-upload/README.md
T

8.3 KiB
Raw Blame History

iPhone → blog, with GitHub Actions

Photos → Share → Publish to blog → optional caption → upload.

The authenticated upload service converts photos to JPEG (maximum 2560 px), applies orientation, converts embedded color profiles to sRGB, and strips public metadata. Originals stay in a private volume. A worker commits optimized images and an _art entry to github.com/mdibaiee/mahdi.blog, on master.

Every push to master triggers .github/workflows/publish.yml. GitHub Actions builds Jekyll and uploads the static output over SSH. nginx switches to the new release only after the transfer completes. Other blog edits use the same workflow. No Jekyll build runs on the server, and Nextcloud is not involved.

GitHub repository configuration

Actions is already enabled; there were no publishing workflows or deployment secrets when inspected. Add these at Settings → Secrets and variables → Actions:

Repository secret Value
DEPLOY_SSH_KEY Private SSH key whose public key is authorized for deployment on theread.me
DEPLOY_KNOWN_HOSTS Verified SSH known_hosts entry for theread.me (include [host]:port if nonstandard)

The workflow has these defaults; override with repository variables if needed:

Variable Default
DEPLOY_HOST theread.me
DEPLOY_USER mahdi
DEPLOY_PORT 22
DEPLOY_PATH /home/mahdi/blog-published

The server login needs write access to DEPLOY_PATH, plus bash, rsync, and flock. Install its public key in that account's ~/.ssh/authorized_keys; use a separate key for this workflow. Verify the SSH host fingerprint independently, not by blindly trusting an unauthenticated scan. The server must accept SSH from GitHub-hosted runners. Deployment itself needs no sudo or nginx reload.

Separately, add the photo publisher's public SSH key at Settings → Deploy keys, with Allow write access enabled for this repository. This key stays on your server and allows photo pushes to trigger Actions; it is not a workflow secret or GITHUB_TOKEN. GitHub is the publishing source of truth. Push ordinary edits to the github remote too; pushing only to git.theread.me does not trigger this workflow.

The workflow uses read-only repository permissions and pinned action commits. It runs only for master, and serialized deployments plus a server-side run-number check prevent an older workflow from replacing a newer deployment. Ruby matches this blog's existing 3.1.2 requirement. The lockfile removes the obsolete jekyll-latex dependency that was already absent from Gemfile, allowing frozen CI installs. Runtime upgrades are separate from this change.

Server setup

The live server is Ubuntu 24.04. nginx currently serves /home/mahdi/theread.me/_site. /home/mahdi/update-server/update-server.js exists, was not running, and uses a different port from nginx's old /_update route. Podman is not installed. The new workflow replaces that rebuild hook.

  1. Install Podman and a Compose provider (sudo apt-get install podman podman-compose).
  2. Keep this service in its own checkout, e.g. /home/mahdi/blog-photo-service.
  3. Prepare private credentials and the public output directory:
install -d -m 700 "$HOME/.config/blog-photos"
install -d -m 755 /home/mahdi/blog-published
umask 077
openssl rand -hex 32 > "$HOME/.config/blog-photos/token"
ssh-keygen -t ed25519 -N '' -f "$HOME/.config/blog-photos/git_key" -C blog-photo-publisher

Register git_key.pub as the repository's write-enabled deploy key. Put GitHub's verified SSH host key in ~/.config/blog-photos/known_hosts. Keep the private key readable only by its owner. This key is separate from Actions' server-login key.

cd /home/mahdi/blog-photo-service/photo-upload
cp .env.example .env
# Adjust absolute paths if needed.
podman compose config
podman compose up -d --build
podman compose logs -f publisher

On SELinux hosts add shared :z labels to bind mounts as appropriate.

Commit and push this change to GitHub, and wait for the first Actions deployment. Then back up nginx's site config and use host-nginx.conf.example: change the HTTPS server's root to /home/mahdi/blog-published/current and add /api/photos. Preserve the existing /, /raw/, TLS, /_xpanel, and unrelated routes. Ensure nginx can traverse parent directories and read the output.

sudo nginx -t
sudo systemctl reload nginx

The API listens on loopback port 8088; HTTPS remains on the existing nginx. Rollback by restoring the original nginx root and reloading. The old checkout and site are untouched. Once verified, retire the old rebuild route.

For reboot persistence, create ~/.config/systemd/user/blog-photos.service:

[Unit]
Description=Blog photo uploads
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/mahdi/blog-photo-service/photo-upload
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose stop
TimeoutStartSec=180

[Install]
WantedBy=default.target

Then run systemctl --user daemon-reload, systemctl --user enable --now blog-photos.service, and sudo loginctl enable-linger mahdi.

iPhone shortcut

Create Publish to blog:

  1. Enable Show in Share Sheet, accepting Images.
  2. Ask for Input (Text): “Caption (optional)”.
  3. Get Contents of URL: https://theread.me/api/photos.
    • Method: POST.
    • Header: Authorization = Bearer YOUR_TOKEN.
    • Request Body: Form.
    • photos: type File, value Shortcut Input (the selected images).
    • caption: type Text, value the caption response.
    • Do not set Content-Type; Shortcuts supplies the multipart boundary.
  4. Show “Uploaded; publishing” on success. Optionally poll the returned status_url with the same header. pushed means “GitHub Actions is building/deploying”; only published means the deployed site's receipt contains this album.

Multi-photo albums require repeated photos parts in one request. Verify that file-list behavior on the target iPhone, starting with one photo and then two. Select still photos; videos/Live Photo video components aren't supported. Dates are upload time in UTC; captions are plain text. A caption is optional.

The token is visible in the shortcut editor; remove it before sharing the shortcut. Rotate it by replacing the token file and recreating the upload container with podman compose up -d --force-recreate upload.

API, failures, and backups

All API routes require the bearer token:

  • POST /api/photos: 1–20 multipart photos, optional caption (4000 characters). Maximum request 100 MiB; each photo at most 60 megapixels.
  • GET /api/photos/<id>: queued, publishing, pushed, published, or failed.
  • POST /api/photos/<id>/retry: retry a failed Git publication.

Identical original bytes, ordering, and caption produce the same ID. Retrying does not duplicate entries or commits. Re-encoding or changing the caption creates a new submission; edit an existing entry through Git. Keep the upload response/status URL.

For Git failures, check podman compose logs publisher, fix access/conflicts, and POST to the status URL plus /retry. No force push is used. If Actions fails, status stays pushed: inspect and rerun the failed workflow on GitHub, rather than uploading again. Deployment only switches nginx after a complete transfer; the old site stays available during builds and failures. The receipt is built from rendered photo anchors so the service does not mistake a successful push for publication.

Back up the private photo-data volume (originals and job state) and GitHub's repository. photo-work is disposable. Never expose private data through nginx or run compose down -v. Originals and deployment releases are retained, so monitor disk usage and periodically remove old releases (never current or an in-progress release). Interrupted uploads may leave files in photo-data/temporary; clean that folder only while the API is stopped. Public Git images are optimized copies.

Tests

python3 -m venv /tmp/blog-photo-tests
/tmp/blog-photo-tests/bin/pip install -r requirements.txt pytest
/tmp/blog-photo-tests/bin/pytest -q tests

Tests use a temporary local bare Git repository; no test pushes go to GitHub or the live server. The iPhone shortcut and production deployment need an end-to-end check after credentials are configured.