Add authenticated photo uploads and GitHub Actions publishing
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# 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:
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
```sh
|
||||
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`:
|
||||
|
||||
```ini
|
||||
[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
|
||||
|
||||
```sh
|
||||
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.
|
||||
Reference in New Issue
Block a user