fix: remove private deployment metadata and retain release licenses
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Successful in 5m8s

This commit is contained in:
渡口浪人
2026-10-18 16:14:56 +08:00
parent 4e9eccd809
commit 038b858eb7
9 changed files with 66 additions and 26 deletions
+3
View File
@@ -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/
+1
View File
@@ -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 \
+4 -4
View File
@@ -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
+14
View File
@@ -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.
+6 -9
View File
@@ -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 规则;本次没有删除。
实际版本、镜像标识、通过 / 失败 / 未执行项见本任务验证记录。
实际部署回执和照片库统计应私下保管,不提交公共仓库。
+2 -2
View File
@@ -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 生成的源码放在 <FRAME_FLOW_ROOT>/migration-inbox 的独立子目录;目录内同时提供 RELEASE_COMMIT、RELEASE_HASHES.json 和原始 source.tar.gz。哈希清单涵盖源码与源码包,不含自身。执行 sudo python3 <暂存目录>/scripts/server-release.py;成功后读取 <FRAME_FLOW_ROOT>/apps/pigway-frame-flow/release-verification.json 并独立检查线上接口。
## 验收范围
完整后端测试、静态检查与桌面/平板/手机的中英文浏览器回归纳入发布检查。线上历史补丁已核验,真实写入保持开启;本轮正式发布不重复真实设备写入测试。隔离浏览器验证与真实设备写入证据分别记录于 docs/verification/2026-10-17-final-audit.md。
完整后端测试、静态检查与桌面/平板/手机的中英文浏览器回归纳入发布检查。线上历史补丁已核验,真实写入保持开启;本轮正式发布不重复真实设备写入测试。隔离浏览器验证与真实设备写入证据由维护者私下保管。
局域网 HTTPS、独立加密备份及恢复演练仍是后续工作;T7 recovery 不属于独立备份。没有执行 USB 断连、服务器重启或压力测试。
+12 -8
View File
@@ -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(
+22 -1
View File
@@ -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()
+2 -2
View File
@@ -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]