From 575fd2669004db5f52a40160ef61b3c53ecae1e3 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 12:31:31 +0900 Subject: [PATCH 1/5] feat(checkout): render identical per-repo allowlist into Hub and native config --- scripts/scripts_cf14.0/cloudfile_bootstrap.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/scripts_cf14.0/cloudfile_bootstrap.py b/scripts/scripts_cf14.0/cloudfile_bootstrap.py index 50a91368..d43f231e 100644 --- a/scripts/scripts_cf14.0/cloudfile_bootstrap.py +++ b/scripts/scripts_cf14.0/cloudfile_bootstrap.py @@ -22,6 +22,7 @@ import json import os import shutil +import uuid from os.path import abspath, basename, dirname, exists, isdir, join # get_proto() is upstream's (SEAFILE_SERVER_PROTOCOL / Letsencrypt aware). It is @@ -126,6 +127,27 @@ def cf_enabled(name): return get_conf(name, default).lower() == 'true' +def _checkout_repo_ids(): + """Canonical UUID allowlist for OPTIONAL Checkout; all other repos are OFF.""" + raw = get_conf('CF_CHECKOUT_REPOS', '') + if len(raw) > 8192: + raise ValueError('CF_CHECKOUT_REPOS exceeds 8192 bytes') + if not raw.strip(): + return () + ids = [] + for item in raw.split(','): + value = item.strip() + try: + if str(uuid.UUID(value)) != value: + raise ValueError('noncanonical UUID') + except (ValueError, AttributeError): + raise ValueError('CF_CHECKOUT_REPOS requires canonical Repo UUIDs') + if value in ids: + raise ValueError('CF_CHECKOUT_REPOS contains a duplicate Repo UUID') + ids.append(value) + return tuple(ids) + + def _replace_block(path, begin, end, body): """Rewrite the region between `begin` and `end`, appending it if absent. @@ -170,6 +192,7 @@ def write_cloudfile_settings(): for name in CF_FEATURE_SWITCHES: body += '%s = %s\n' % (name, cf_enabled(name)) + body += 'CF_CHECKOUT_REPOS = %r\n' % (_checkout_repo_ids(),) # 只写标量,不要碰 DATABASES。 # @@ -816,6 +839,7 @@ def _seafile_conf_cloudfile_lines(): key = name[len('CF_ENABLE_'):].lower() + '_enabled' lines.append('%s = %s\n' % (key, 'true' if cf_enabled(name) else 'false')) + lines.append('checkout_repo_ids = %s\n' % ','.join(_checkout_repo_ids())) if cf_enabled('CF_ENABLE_FILE_LOCK'): backend = get_conf('CF_LOCK_BACKEND', 'cloudfile').lower() From 8df120cef3c1928dcb7b4ea4dc112985af38735b Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 12:31:34 +0900 Subject: [PATCH 2/5] chore(checkout): share allowlist with web and worker environments --- deploy/compose/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index da83e896..8e7f078f 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -213,6 +213,7 @@ services: JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY:-} CF_LOCK_BACKEND: ${CF_LOCK_BACKEND:-cloudfile} CF_ENABLE_CHECKOUT: ${CF_ENABLE_CHECKOUT:-false} + CF_CHECKOUT_REPOS: ${CF_CHECKOUT_REPOS:-} # Keep explicit policy/transfer settings effective on both app and worker. # Native administrator policy is initialized automatically from existing SQL settings. CF_ENABLE_LOCAL_APP: ${CF_ENABLE_LOCAL_APP:-false} @@ -483,6 +484,7 @@ services: CF_ENABLE_CONVERT_EXPORT: ${CF_ENABLE_CONVERT_EXPORT:-false} CF_LOCK_BACKEND: ${CF_LOCK_BACKEND:-cloudfile} CF_ENABLE_CHECKOUT: ${CF_ENABLE_CHECKOUT:-false} + CF_CHECKOUT_REPOS: ${CF_CHECKOUT_REPOS:-} # Keep explicit policy/transfer settings effective on both app and worker. # Native administrator policy is initialized automatically from existing SQL settings. CF_ENABLE_LOCAL_APP: ${CF_ENABLE_LOCAL_APP:-false} From 7c5c56d10ded0ce649bff8cb41e4539534907c33 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 12:31:38 +0900 Subject: [PATCH 3/5] docs(checkout): document default OFF and explicit OPTIONAL Repo UUID list --- deploy/compose/.env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deploy/compose/.env.example b/deploy/compose/.env.example index 60b8d633..17dfa4a7 100644 --- a/deploy/compose/.env.example +++ b/deploy/compose/.env.example @@ -99,6 +99,13 @@ CF_ENABLE_WATCH=true CF_ENABLE_CONVERT_EXPORT=false CF_LOCK_BACKEND=cloudfile CF_ENABLE_CHECKOUT=false +# OPTIONAL Checkout is enabled only for these exact Seafile Repo UUIDs. +# Empty (default) = OFF for every library, even when CF_ENABLE_CHECKOUT=true. +# No REQUIRED mode and no extension-based auto Checkout. Examples: +# CF_CHECKOUT_REPOS=11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222 +# Changing the list requires a coordinated restart of Hub / native C / Go. +# Remove a library only after its active Guard leases and pending edits are resolved. +CF_CHECKOUT_REPOS= CLOUDFILE_EDITING_ENABLED=false CLOUDFILE_RESOURCE_SECRET= CF_ENABLE_LOCAL_APP=false From 0ab62adc74a11b5b270d41d6ebb2fd4d1dd67f1b Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 12:33:26 +0900 Subject: [PATCH 4/5] test(checkout): focused OFF and OPTIONAL config checks --- tests/test_checkout_repo_config.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_checkout_repo_config.py diff --git a/tests/test_checkout_repo_config.py b/tests/test_checkout_repo_config.py new file mode 100644 index 00000000..a3f67a27 --- /dev/null +++ b/tests/test_checkout_repo_config.py @@ -0,0 +1,57 @@ +"""Isolated validation of the OFF / OPTIONAL repository configuration. + +No Docker, database, or CloudFile runtime is needed. +""" +import ast +import uuid +import unittest +from pathlib import Path + +SOURCE = Path(__file__).resolve().parents[1] / 'scripts/scripts_cf14.0/cloudfile_bootstrap.py' +TREE = ast.parse(SOURCE.read_text()) +METHODS = {'_checkout_repo_ids', '_seafile_conf_cloudfile_lines'} +FUNCTIONS = [node for node in TREE.body if isinstance(node, ast.FunctionDef) and node.name in METHODS] + + +class CheckoutRepoConfigTest(unittest.TestCase): + def setUp(self): + self.env = {} + self.namespace = { + 'uuid': uuid, + 'get_conf': lambda name, default='': self.env.get(name, default), + 'cf_enabled': lambda name: self.env.get(name, 'false') == 'true', + 'CF_FEATURE_SWITCHES': ('CF_ENABLE_CHECKOUT',), + } + exec(compile(ast.Module(body=FUNCTIONS, type_ignores=[]), str(SOURCE), 'exec'), + self.namespace) + + def test_no_selection_means_all_off(self): + self.assertEqual(self.namespace['_checkout_repo_ids'](), ()) + rendered = ''.join(self.namespace['_seafile_conf_cloudfile_lines']()) + self.assertIn('checkout_enabled = false\n', rendered) + self.assertIn('checkout_repo_ids = \n', rendered) + + def test_global_switch_alone_does_not_enable_any_repo(self): + self.env['CF_ENABLE_CHECKOUT'] = 'true' + self.assertEqual(self.namespace['_checkout_repo_ids'](), ()) + self.assertIn('checkout_repo_ids = \n', + ''.join(self.namespace['_seafile_conf_cloudfile_lines']())) + + def test_selected_repo_is_rendered_once(self): + repo = '11111111-1111-4111-8111-111111111111' + self.env.update(CF_ENABLE_CHECKOUT='true', CF_CHECKOUT_REPOS=repo) + self.assertEqual(self.namespace['_checkout_repo_ids'](), (repo,)) + self.assertIn('checkout_repo_ids = ' + repo + '\n', + ''.join(self.namespace['_seafile_conf_cloudfile_lines']())) + + def test_invalid_lists_rejected_before_config_write(self): + good = '11111111-1111-4111-8111-111111111111' + for value in ['not-a-uuid', good + ',', good + ',' + good, + 'AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA', 'x' * 8193]: + self.env['CF_CHECKOUT_REPOS'] = value + with self.subTest(value=value[:40]), self.assertRaises(ValueError): + self.namespace['_checkout_repo_ids']() + + +if __name__ == '__main__': + unittest.main() From 102eedd10a59c10af05e15f8dd8c5019eeb1e736 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 12:33:29 +0900 Subject: [PATCH 5/5] docs(checkout): accept only OFF and OPTIONAL per repository --- deploy/compose/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 6c85ce4f..a106ef49 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -37,3 +37,33 @@ docker compose logs -f cloudfile 镜像与发行包由 `build/cloudfile_14.0/build-in-docker.sh` + `image/cloudfile_14.0/docker-build.sh` 生成,来源 SHA 由根目录 `release.yaml` 解析后写入 `build-receipt.json`。 入口说明见根目录 `README.md`、`CLOUDFILE.md`、`AGENTS.md`。 + + +## 库级 Checkout(OFF / OPTIONAL) + +普通文件库默认 **OFF**,只使用 Seafile 原生版本与权限能力,不进入 Guard SQL; +专业库可显式设为 **OPTIONAL**,允许用户对单个文件主动 Checkout、Commit、 +Checkin。本产品**不提供 REQUIRED**(强制全部签出)模式。 + +`CF_ENABLE_CHECKOUT=false` 是全局总开关(默认);开启总开关后, +仍然**只**对 `CF_CHECKOUT_REPOS` 中列出的 **Repo UUID** 启用 OPTIONAL: + +```env +CF_ENABLE_CHECKOUT=true +CF_CHECKOUT_REPOS=11111111-1111-4111-8111-111111111111 +``` + +`CF_CHECKOUT_REPOS` 为空则所有库保持 OFF。每个 UUID 必须是标准小写形式, +多库用逗号隔开。配置启动时校验并同步生成 Hub 与原生 `seafile.conf` +(Go/C 使用 `checkout_repo_ids`);变更需协调重启对应服务。 +无新表、库级运行时数据库查询、文件类型自动切换或动态策略后台。 + +**运维变更纪律:** 将 OPTIONAL 库移出列表或关闭总开关之前,先停止新签出, +完成/取消该库所有活动 Checkout、处理 Agent 本地待确认任务,并确认不存在 +未过期的 Guard 租约。不能通过关闭能力绕过尚未完成的独占编辑。 +原先全局启用 Checkout 的部署升级时,必须先将仍有活动锁的库 +纳入白名单,并在统一重启前完成核对。 + +仅 OPTIONAL 库依赖 Guard 表;Guard 失效时对应库拒绝不安全写入, +但未启用的库继续普通文件操作。与 Hub PR #50、 +Agent2 #10 / #1 联动验收后再向 PROD 发布。