diff --git a/.gitignore b/.gitignore index 6756f07..5e24a17 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ build-cache/wheels/* # Private browser captures and platform metadata docs/screenshots/ .DS_Store + +# Private deployment receipts belong outside public source control. +docs/verification/ diff --git a/deploy/Dockerfile.runtime-update b/deploy/Dockerfile.runtime-update index 37a8fba..0443f4a 100644 --- a/deploy/Dockerfile.runtime-update +++ b/deploy/Dockerfile.runtime-update @@ -8,6 +8,7 @@ COPY requirements.lock /app/requirements.expected.lock RUN python -c "from pathlib import Path; assert Path('/app/requirements.lock').read_bytes() == Path('/app/requirements.expected.lock').read_bytes(), 'Dependency lock changed; use the full Dockerfile'" COPY pigway_frame_flow /app/pigway_frame_flow COPY vendor /app/vendor +COPY LICENSE NOTICE.md /app/ # Publish only the cleaned filesystem, without retaining build-stage layers. RUN rm -rf /usr/local/lib/python*/site-packages/pip \ /usr/local/lib/python*/site-packages/pip-*.dist-info \ diff --git a/deploy/compose.ubuntu-photos-api.yml b/deploy/compose.ubuntu-photos-api.yml index 2676622..00ea824 100644 --- a/deploy/compose.ubuntu-photos-api.yml +++ b/deploy/compose.ubuntu-photos-api.yml @@ -9,8 +9,8 @@ services: init: true user: "10333:10333" ports: - - "${FRAME_FLOW_BIND:-192.0.2.10}:81:18787" - env_file: ${FRAME_FLOW_ENV_FILE:-/srv/pigway/apps/pigway-frame-flow/.env} + - "${FRAME_FLOW_BIND:-127.0.0.1}:81:18787" + env_file: ${FRAME_FLOW_ENV_FILE:?Set the private environment file path} environment: TZ: Asia/Shanghai DATA_DIR: /data @@ -23,12 +23,12 @@ services: TMPDIR: /data/tmp volumes: - type: bind - source: /srv/pigway/apps/pigway-frame-flow/data + source: ${FRAME_FLOW_APP_DIR:?Set the existing application directory}/data target: /data bind: create_host_path: false - type: bind - source: /srv/pigway/apps/pigway-frame-flow/config + source: ${FRAME_FLOW_APP_DIR:?Set the existing application directory}/config target: /config bind: create_host_path: false diff --git a/docs/PUBLICATION.md b/docs/PUBLICATION.md index 5ce3200..b14c2af 100644 --- a/docs/PUBLICATION.md +++ b/docs/PUBLICATION.md @@ -20,3 +20,17 @@ Publishing requires scanning all refs, removing private historic files, pruning unreachable remote Git objects and archiving/removing historical Actions logs. A private original mirror is retained outside the repository. Existing clones must reclone after the privacy history rewrite; do not merge the original history back. + +## Private deployment inputs + +Do not publish host addresses, workstation paths, disk UUIDs, photo-library counts, +or deployment receipts. Keep receipts and recovery bundles outside the repository +in an owner-only directory. A privacy history rewrite changes existing commit IDs; +existing clones must be replaced instead of merging old history back. + +`server-release.py` reads `FRAME_FLOW_ROOT` (default `/srv/frameflow`), requires +`FRAME_FLOW_MOUNT_UUID` and `FRAME_FLOW_ORIGIN`, and retains the mount-identity +check before release actions. Supply verified values privately to the sudo process; +existing installations must set their existing root explicitly. Do not use the +generic default to migrate a deployment. The script still requires the existing +mount guard and does not change host mounts, services, or storage automatically. diff --git a/docs/SYNOLOGY_PHOTOS_API.md b/docs/SYNOLOGY_PHOTOS_API.md index b72a641..136d549 100644 --- a/docs/SYNOLOGY_PHOTOS_API.md +++ b/docs/SYNOLOGY_PHOTOS_API.md @@ -10,7 +10,7 @@ - 支持目录相对路径、递归开关、日期文件夹 / 标题、方向及现有质量筛选。`.` 表示所选空间根目录;路径以 Photos 返回的目录名为准,不是 Ubuntu 绝对路径。 - 图片使用经过实测的 `Thumbnail.get` v2、`size=xl`。原始文件名不代表下载的是原图。对 NAS 当前原图下载接口的探测被拒绝,原图下载**未宣称支持**。 - 照片枚举 `Browse.Item.list` v7,人物 `Browse.Person.list` v3;每批 500 条,最多 100000 条,超过明确失败,界面目录上限仍由 `catalog_limit` 控制。不支持 NAS 未暴露或账号无权访问的相册。 -- 旧 `local` 来源保留挂载读取方式,不自动变成 API 来源。新建 / 编辑来源时可明确选择 API 类型。 +- 文件系统 `local` 来源已停用;旧配置需显式转换为 Photos API 来源。 ## 安全与缓存 @@ -22,14 +22,11 @@ ## Ubuntu 候选配置 -正式代码在 `/home/developer/Documents/Projects/pigway-frame-flow` 的 `codex/synology-photos-api` 分支开发,不能以迁移临时解包目录作为开发仓库。 +在正式 Git 仓库开发;不要把迁移临时解包目录作为开发仓库。 -候选文件:`deploy/compose.ubuntu-photos-api.yml`。外部 `192.0.2.10:81`,内部 `18787`,UID/GID `10333:10333`,数据和配置固定为: +候选文件:`deploy/compose.ubuntu-photos-api.yml`。内部端口 `18787`,UID/GID `10333:10333`。通过私有环境变量设置 `FRAME_FLOW_APP_DIR`(已有应用目录)、`FRAME_FLOW_ENV_FILE`(已有私有配置文件)和可选 `FRAME_FLOW_BIND`(默认回环地址),不在仓库保存真实主机地址或部署目录。 -- `/srv/pigway/apps/pigway-frame-flow/data:/data` -- `/srv/pigway/apps/pigway-frame-flow/config:/config` - -无 `/photos` 绑定。`create_host_path: false` 防止在 T7 未挂载时悄悄创建目录;但仍需在启动前检查 `findmnt -T /srv/pigway` 的实际源设备。 +无 `/photos` 绑定。`create_host_path: false` 防止在 T7 未挂载时悄悄创建目录;但仍需在启动前检查 `findmnt -T "$FRAME_FLOW_APP_DIR"` 的实际源设备。 `.env` 需从已确认的配置安全迁移,保留 Photos URL、证书 pin 与已有 Frame Flow / 相框鉴权;不要把密钥写入仓库。模板默认 `KOKONNA_WRITE_ENABLED=false`。创建 `data/tmp`,属主保持 10333:10333,`TMPDIR=/data/tmp`。Docker data-root 和 containerd root 也必须在 T7,不能仅检查 bind mount。 @@ -60,6 +57,6 @@ FRAME_FLOW_IMAGE='<已验证的镜像标签>' docker compose -f deploy/compose.u ## NFS 授权判断 -API 来源的共享 / 个人空间取图已经可以完全不依赖 NFS,因此这条新取图链路不需要为 `192.0.2.10` 保留 DSM `photo` NFS 授权。但目前旧来源和待迁移配置没有全部切换验收,不能据此宣称迁移完成或旧本地来源已可脱离挂载。确认没有其他已批准用途后,可以单独授权删除该特定 NFS 规则;本次没有删除。 +API 来源的共享 / 个人空间取图已经可以完全不依赖 NFS,因此这条新取图链路不需要为旧客户端保留 DSM `photo` NFS 授权。但目前旧来源和待迁移配置没有全部切换验收,不能据此宣称迁移完成或旧本地来源已可脱离挂载。确认没有其他已批准用途后,可以单独授权删除该特定 NFS 规则;本次没有删除。 -实际版本、镜像标识、通过 / 失败 / 未执行项见本任务验证记录。 +实际部署回执和照片库统计应私下保管,不提交公共仓库。 diff --git a/docs/releases/v0.1.9.md b/docs/releases/v0.1.9.md index 10852c9..0c3cb90 100644 --- a/docs/releases/v0.1.9.md +++ b/docs/releases/v0.1.9.md @@ -10,10 +10,10 @@ 正式发布使用 scripts/server-release.py:仅在既有 Ubuntu Server 环境运行,从已提交源码包构建,校验依赖锁、源码哈希、T7 UUID、容器安全约束和真实写入设置;备份 SQLite、配置及 Compose 后切换镜像,失败恢复旧 Compose,不自动回写旧数据库。它不执行上传、显示、删除或恢复图片等设备操作。成功后记录正式提交号、保存源码包及审计回执,删除本次暂存目录;白名单内的 Frame 旧迁移输入归档到本次 recovery,清除无恢复引用的临时镜像别名,共享 Console 资料不动。旧镜像及历史恢复资料保留供回滚,不执行全局 prune。 -脚本要求由 git archive 生成的源码放在 /srv/pigway/migration-inbox 的独立子目录;目录内同时提供 RELEASE_COMMIT、RELEASE_HASHES.json 和原始 source.tar.gz。哈希清单涵盖源码与源码包,不含自身。执行 sudo python3 <暂存目录>/scripts/server-release.py;成功后读取 /srv/pigway/apps/pigway-frame-flow/release-verification.json 并独立检查线上接口。 +脚本要求由 git archive 生成的源码放在 /migration-inbox 的独立子目录;目录内同时提供 RELEASE_COMMIT、RELEASE_HASHES.json 和原始 source.tar.gz。哈希清单涵盖源码与源码包,不含自身。执行 sudo python3 <暂存目录>/scripts/server-release.py;成功后读取 /apps/pigway-frame-flow/release-verification.json 并独立检查线上接口。 ## 验收范围 -完整后端测试、静态检查与桌面/平板/手机的中英文浏览器回归纳入发布检查。线上历史补丁已核验,真实写入保持开启;本轮正式发布不重复真实设备写入测试。隔离浏览器验证与真实设备写入证据分别记录于 docs/verification/2026-10-17-final-audit.md。 +完整后端测试、静态检查与桌面/平板/手机的中英文浏览器回归纳入发布检查。线上历史补丁已核验,真实写入保持开启;本轮正式发布不重复真实设备写入测试。隔离浏览器验证与真实设备写入证据由维护者私下保管。 局域网 HTTPS、独立加密备份及恢复演练仍是后续工作;T7 recovery 不属于独立备份。没有执行 USB 断连、服务器重启或压力测试。 diff --git a/scripts/server-release.py b/scripts/server-release.py index 3d9dc91..d0a6195 100644 --- a/scripts/server-release.py +++ b/scripts/server-release.py @@ -13,10 +13,11 @@ import time import urllib.request from pathlib import Path -APP = Path("/srv/pigway/apps/pigway-frame-flow") -RECOVERY = Path("/srv/pigway/recovery/frame-flow") +ROOT = Path(os.getenv("FRAME_FLOW_ROOT", "/srv/frameflow")) +APP = ROOT / "apps/pigway-frame-flow" +RECOVERY = ROOT / "recovery/frame-flow" CONTAINER = "pigway-frame-flow" -ORIGIN = "http://192.0.2.10:81" +ORIGIN = os.getenv("FRAME_FLOW_ORIGIN", "http://127.0.0.1:6005").rstrip("/") def run(*args): @@ -112,11 +113,14 @@ def verify_runtime(commit): def main(): assert os.geteuid() == 0, "Run with sudo on the Ubuntu Server" + expected_uuid = os.environ.get("FRAME_FLOW_MOUNT_UUID", "") + assert expected_uuid, "Set FRAME_FLOW_MOUNT_UUID to the verified storage UUID" + assert os.environ.get("FRAME_FLOW_ORIGIN"), ( + "Set FRAME_FLOW_ORIGIN to the verified listener" + ) os.umask(0o077) run("/usr/local/libexec/pigway-guard-mount") - assert run("findmnt", "-n", "-o", "UUID", "/srv/pigway") == ( - "00000000-0000-4000-8000-000000000001" - ) + assert run("findmnt", "-n", "-o", "UUID", str(ROOT)) == expected_uuid with (RECOVERY / ".release.lock").open("a") as lock: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) release(lock) @@ -124,7 +128,7 @@ def main(): def release(lock): ctx = Path(__file__).resolve().parent.parent - assert ctx.parent == Path("/srv/pigway/migration-inbox") + assert ctx.parent == ROOT / "migration-inbox" commit = (ctx / "RELEASE_COMMIT").read_text().strip() assert re.fullmatch("[0-9a-f]{40}", commit) hashes = json.loads((ctx / "RELEASE_HASHES.json").read_text()) @@ -193,7 +197,7 @@ def release(lock): n: h for n, h in hashes.items() if n.startswith(("pigway_frame_flow/", "vendor/")) - or n == "requirements.lock" + or n in {"requirements.lock", "LICENSE", "NOTICE.md"} } code = 'import hashlib,json,pathlib,sys; h=json.load(sys.stdin); assert all(hashlib.sha256((pathlib.Path("/app")/n).read_bytes()).hexdigest()==v for n,v in h.items())' subprocess.run( diff --git a/tests/test_server_release.py b/tests/test_server_release.py index f9f714f..91daead 100644 --- a/tests/test_server_release.py +++ b/tests/test_server_release.py @@ -63,7 +63,7 @@ def test_release_readiness_retries_on_actual_listener(release_tool, monkeypatch) def listener(url, timeout): calls.append(url) - if url != "http://192.0.2.10:81/ready" or len(calls) == 1: + if url != release_tool.ORIGIN + "/ready" or len(calls) == 1: raise urllib.error.URLError("Connection refused during restart") return io.BytesIO(b'{"status":"ready"}') @@ -71,3 +71,24 @@ def test_release_readiness_retries_on_actual_listener(release_tool, monkeypatch) monkeypatch.setattr(release_tool.time, "sleep", lambda _: None) release_tool.wait_ready() assert len(calls) == 2 + + +def test_release_requires_private_storage_identity_before_host_actions( + release_tool, monkeypatch +): + calls = [] + monkeypatch.setattr(release_tool.os, "geteuid", lambda: 0) + monkeypatch.setattr(release_tool, "run", lambda *args: calls.append(args)) + monkeypatch.delenv("FRAME_FLOW_MOUNT_UUID", raising=False) + with pytest.raises(AssertionError, match="FRAME_FLOW_MOUNT_UUID"): + release_tool.main() + assert calls == [] + + +def test_release_rejects_wrong_storage_uuid(release_tool, monkeypatch): + monkeypatch.setattr(release_tool.os, "geteuid", lambda: 0) + monkeypatch.setenv("FRAME_FLOW_MOUNT_UUID", "expected-storage-uuid") + monkeypatch.setenv("FRAME_FLOW_ORIGIN", release_tool.ORIGIN) + monkeypatch.setattr(release_tool, "run", lambda *args: "different-storage-uuid") + with pytest.raises(AssertionError): + release_tool.main() diff --git a/tests/test_synology_photos_api.py b/tests/test_synology_photos_api.py index a6e69e6..c358d44 100644 --- a/tests/test_synology_photos_api.py +++ b/tests/test_synology_photos_api.py @@ -69,12 +69,12 @@ def test_people_pagination_does_not_stop_at_500(settings, monkeypatch): return { "list": [ {"id": i} - for i in range(params["offset"], min(1334, params["offset"] + 500)) + for i in range(params["offset"], min(1203, params["offset"] + 500)) ] } monkeypatch.setattr(p, "request", request) - assert len(p.people()) == 1334 + assert len(p.people()) == 1203 assert calls == [0, 500, 1000]