Compare commits
3 Commits
6b3ab9f651
...
5ca92fec7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ca92fec7b | |||
| 5f8d6ce9ad | |||
| 493dab0212 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,3 +5,7 @@ photo_pipeline.db
|
|||||||
*.db-wal
|
*.db-wal
|
||||||
*.db-shm
|
*.db-shm
|
||||||
test_flow.py
|
test_flow.py
|
||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
.immich-key
|
||||||
|
|||||||
116
immich_import.py
Normal file
116
immich_import.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""photo-pipeline: Immich import flow (v5).
|
||||||
|
|
||||||
|
Uploads sorted/staged photos to Immich using the SERVER-BUNDLED CLI
|
||||||
|
(version-matched, verified working — npm CLI 3.1.0 had a deviceAssetId bug
|
||||||
|
against server 2.7.5).
|
||||||
|
|
||||||
|
Approach: on .35, docker cp staging dir into immich_server container, then
|
||||||
|
docker exec immich_server immich upload.
|
||||||
|
|
||||||
|
Security: API key via env IMMICH_API_KEY or ~/photo-pipeline/.immich-key (chmod 600).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from prefect import flow, task
|
||||||
|
|
||||||
|
IMMICH_HOST = "192.168.20.35"
|
||||||
|
IMMICH_URL = os.environ.get("IMMICH_URL", f"http://{IMMICH_HOST}:2283")
|
||||||
|
KEY_FILE = Path(__file__).parent / ".immich-key"
|
||||||
|
CONTAINER = "immich_server"
|
||||||
|
|
||||||
|
|
||||||
|
def _api_key() -> str:
|
||||||
|
key = os.environ.get("IMMICH_API_KEY")
|
||||||
|
if key:
|
||||||
|
return key
|
||||||
|
if KEY_FILE.exists():
|
||||||
|
return KEY_FILE.read_text().strip()
|
||||||
|
raise RuntimeError("IMMICH_API_KEY env or ~/photo-pipeline/.immich-key required")
|
||||||
|
|
||||||
|
|
||||||
|
def _ssh(args: list[str], timeout: int = 600) -> subprocess.CompletedProcess:
|
||||||
|
# -i: .35 accepts id_ed25519_rsync (used by rsync backups); default key rejected
|
||||||
|
return subprocess.run(
|
||||||
|
["ssh", "-i", os.path.expanduser("~/.ssh/id_ed25519_rsync"),
|
||||||
|
"sam@" + IMMICH_HOST, *args],
|
||||||
|
capture_output=True, text=True, timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def verify_immich():
|
||||||
|
"""Sanity check: server reachable + key works."""
|
||||||
|
r = subprocess.run(
|
||||||
|
["curl", "-s", "-H", f"x-api-key: {_api_key()}",
|
||||||
|
f"{IMMICH_URL}/api/server/version"],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
)
|
||||||
|
print(f"Immich server version: {r.stdout.strip()}")
|
||||||
|
if "error" in r.stdout.lower() or r.returncode != 0:
|
||||||
|
raise RuntimeError(f"Immich unreachable or key rejected: {r.stdout}")
|
||||||
|
return r.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def upload_to_immich(src_dir: str, container_path: str = "/import") -> dict:
|
||||||
|
"""Copy src_dir into the immich_server container and upload via bundled CLI.
|
||||||
|
|
||||||
|
Two-hop: scp src_dir from .13 → /tmp on .35, then docker cp into container,
|
||||||
|
then docker exec immich upload (version-matched CLI).
|
||||||
|
"""
|
||||||
|
key = _api_key()
|
||||||
|
ssh_args = ["-i", os.path.expanduser("~/.ssh/id_ed25519_rsync")]
|
||||||
|
|
||||||
|
# 1. scp the staged dir to .35 /tmp
|
||||||
|
local_dir = Path(src_dir)
|
||||||
|
remote_tmp = f"/tmp/pp_import_{local_dir.name}"
|
||||||
|
r = subprocess.run(
|
||||||
|
["scp", *ssh_args, "-r", str(local_dir), f"sam@{IMMICH_HOST}:{remote_tmp}"],
|
||||||
|
capture_output=True, text=True, timeout=900,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"scp to .35 failed: {r.stderr[-500:]}")
|
||||||
|
print(f"scp OK → {remote_tmp}")
|
||||||
|
|
||||||
|
# 2. docker cp into container (clear first)
|
||||||
|
r = _ssh(["docker", "exec", CONTAINER, "rm", "-rf", container_path])
|
||||||
|
r = _ssh(["docker", "cp", f"{remote_tmp}/.", f"{CONTAINER}:{container_path}"])
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"docker cp failed: {r.stderr[-500:]}")
|
||||||
|
print(f"docker cp OK → {container_path}")
|
||||||
|
|
||||||
|
# 3. upload via bundled CLI inside container
|
||||||
|
r = _ssh([
|
||||||
|
"docker", "exec",
|
||||||
|
"-e", "IMMICH_INSTANCE_URL=http://localhost:2283",
|
||||||
|
"-e", f"IMMICH_API_KEY={key}",
|
||||||
|
CONTAINER, "immich", "upload", container_path,
|
||||||
|
])
|
||||||
|
out = r.stdout
|
||||||
|
print(out[-2000:])
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"immich upload failed: {r.stderr[-500:]}")
|
||||||
|
# cleanup remote temp
|
||||||
|
_ssh(["rm", "-rf", remote_tmp])
|
||||||
|
return {"output": out[-1000:]}
|
||||||
|
|
||||||
|
|
||||||
|
@flow(name="immich-import")
|
||||||
|
def immich_import(src_dir: str, dry_run: bool = True):
|
||||||
|
"""Import a staged folder into Immich via the server's bundled CLI."""
|
||||||
|
ver = verify_immich()
|
||||||
|
if dry_run:
|
||||||
|
print(f"[dry-run] would upload {src_dir} to Immich (server {ver})")
|
||||||
|
return {"dry_run": True, "server": ver}
|
||||||
|
return upload_to_immich(src_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
src = sys.argv[1] if len(sys.argv) > 1 else "/mnt/data/01_keep"
|
||||||
|
dry = "--real" not in sys.argv # default dry-run; pass --real to actually upload
|
||||||
|
immich_import(src, dry_run=dry)
|
||||||
Reference in New Issue
Block a user