From 74a73c5a41e925d3ff9d274a7da744cffd143f23 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:03:15 +0900 Subject: [PATCH 1/7] feat(worker): run periodic org catalog sync alongside optional Meilisearch, no extra service --- deploy/compose/docker-compose.yml | 48 ++++++++++++++++--------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index 8e7f078f..3cd60e04 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -298,6 +298,7 @@ services: CF_PROVIDER_SSO_DIRECTORY: ${CF_PROVIDER_SSO_DIRECTORY:-} CF_SSO_GROUP_OWNER: ${CF_SSO_GROUP_OWNER:-} CF_SSO_SYNC_INTERVAL: ${CF_SSO_SYNC_INTERVAL:-600} + CF_DIRECTORY_SYNC_INTERVAL_SECONDS: ${CF_DIRECTORY_SYNC_INTERVAL_SECONDS:-600} CF_SSO_MAX_REMOVAL_RATIO: ${CF_SSO_MAX_REMOVAL_RATIO:-0.5} CF_SSO_DIRECTORY_STATIC: ${CF_SSO_DIRECTORY_STATIC:-} CF_SERVICE_SSO_DIRECTORY_URL: ${CF_SERVICE_SSO_DIRECTORY_URL:-} @@ -395,24 +396,13 @@ services: condition: service_healthy restart: unless-stopped - # Background worker. Two mutually exclusive jobs, chosen by configuration: - # - # CF_PROVIDER_SEARCH=meilisearch -> the V2 Meilisearch indexer - # (`python3 -m cloudfile_ext.v2.search.worker`, checkpoint - # /shared/search-v2-checkpoint.json). Meilisearch answers queries but never - # indexes CloudFile's derived documents itself, so this process is what - # keeps the index current: it loops every 60s, confirms each index task, - # and a failed batch keeps its checkpoint for retry. - # - # otherwise -> `manage.py cf_worker`, which loops over the registry's - # periodic tasks and exits 0 when none is registered. The V2 runtime - # registers none today -- SSO directory sync is the push trigger - # POST /api/v2.1/admin/cloudfile/directory/entities/sync/ -- so this branch - # is a clean idle exit. - # - # Safe to keep in the default stack: it waits for cloudfile to be healthy, and - # restarts on failure only, so a clean idle exit stays exited instead of - # looping. + # Background worker. The existing cf_worker handles periodic tasks (now + # including a 10-minute enterprise org/role catalog sync). The optional + # Meilisearch indexer can run alongside it in this SAME container when + # enterprise Directory is enabled: no separate scheduler service/table. + # Failures in either long-running process restart the worker container. + # If neither indexing nor periodic tasks are configured, the clean idle + # cf_worker exit stays exited (restart: on-failure). cf-worker: image: ${CLOUDFILE_IMAGE:-cloudfile/cloudfile:14.0.0-cf.0} container_name: cloudfile-worker @@ -422,12 +412,23 @@ services: - > /scripts/create_data_links.sh && if [ "$${CF_PROVIDER_SEARCH:-}" = "meilisearch" ]; then - export DJANGO_SETTINGS_MODULE=seahub.settings && - exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; + export DJANGO_SETTINGS_MODULE=seahub.settings; + if [ -n "$${CF_SERVICE_SSO_DIRECTORY_URL:-}" ]; then + /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env + python3 /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub/manage.py cf_worker & + periodic_pid=$!; + /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env + python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json & + search_pid=$!; + trap 'kill "${periodic_pid}" "${search_pid}" 2>/dev/null || true' EXIT TERM INT; + wait -n "${periodic_pid}" "${search_pid}"; + exit 1; + fi; + exec /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env + python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; fi; - exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker + exec /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env + python3 /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub/manage.py cf_worker volumes: - ./data/seafile:/shared environment: @@ -521,6 +522,7 @@ services: CF_PROVIDER_SSO_DIRECTORY: ${CF_PROVIDER_SSO_DIRECTORY:-} CF_SSO_GROUP_OWNER: ${CF_SSO_GROUP_OWNER:-} CF_SSO_SYNC_INTERVAL: ${CF_SSO_SYNC_INTERVAL:-600} + CF_DIRECTORY_SYNC_INTERVAL_SECONDS: ${CF_DIRECTORY_SYNC_INTERVAL_SECONDS:-600} CF_SSO_MAX_REMOVAL_RATIO: ${CF_SSO_MAX_REMOVAL_RATIO:-0.5} CF_SSO_DIRECTORY_STATIC: ${CF_SSO_DIRECTORY_STATIC:-} CF_SERVICE_SSO_DIRECTORY_URL: ${CF_SERVICE_SSO_DIRECTORY_URL:-} From f007fa3595c2a67f5525e8c49195ed41caf9871d Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:03:40 +0900 Subject: [PATCH 2/7] fix(compose): escape all worker runtime shell variables from Compose interpolation --- deploy/compose/docker-compose.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index 3cd60e04..05b8c251 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -414,21 +414,21 @@ services: if [ "$${CF_PROVIDER_SEARCH:-}" = "meilisearch" ]; then export DJANGO_SETTINGS_MODULE=seahub.settings; if [ -n "$${CF_SERVICE_SSO_DIRECTORY_URL:-}" ]; then - /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env - python3 /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub/manage.py cf_worker & - periodic_pid=$!; - /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env + /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env + python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker & + periodic_pid=$$!; + /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json & - search_pid=$!; - trap 'kill "${periodic_pid}" "${search_pid}" 2>/dev/null || true' EXIT TERM INT; - wait -n "${periodic_pid}" "${search_pid}"; + search_pid=$$!; + trap 'kill "$${periodic_pid}" "$${search_pid}" 2>/dev/null || true' EXIT TERM INT; + wait -n "$${periodic_pid}" "$${search_pid}"; exit 1; fi; - exec /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env + exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; fi; - exec /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub.sh python-env - python3 /opt/seafile/${SEAFILE_SERVER}-${SEAFILE_VERSION}/seahub/manage.py cf_worker + exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env + python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker volumes: - ./data/seafile:/shared environment: From a5fd69f4051e808cae7ee69bac7dbea5af9022f1 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:03:54 +0900 Subject: [PATCH 3/7] feat(config): expose default 600s enterprise catalog cadence to Hub and worker --- scripts/scripts_cf14.0/cloudfile_bootstrap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/scripts_cf14.0/cloudfile_bootstrap.py b/scripts/scripts_cf14.0/cloudfile_bootstrap.py index d43f231e..ab6aff2b 100644 --- a/scripts/scripts_cf14.0/cloudfile_bootstrap.py +++ b/scripts/scripts_cf14.0/cloudfile_bootstrap.py @@ -219,6 +219,10 @@ def write_cloudfile_settings(): ) body += _settings_block_sso() + # V2 enterprise org/role catalog worker interval; one low-frequency task. + # Malformed values fail startup through the Hub's bounded setting gate. + body += 'CF_DIRECTORY_SYNC_INTERVAL_SECONDS = %r\\n' % int( + get_conf('CF_DIRECTORY_SYNC_INTERVAL_SECONDS', '600')) # EAP may use the existing OAuth/SSO bridge with the legacy full # group-synchronizer disabled. When the new one-shot administrator sync is # explicitly enabled, still expose its trusted Directory connection. From 61094cc6ab136abd1e58e6b24a9b2d652304e788 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:04:14 +0900 Subject: [PATCH 4/7] fix: write generated Django setting with a real newline escape --- scripts/scripts_cf14.0/cloudfile_bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/scripts_cf14.0/cloudfile_bootstrap.py b/scripts/scripts_cf14.0/cloudfile_bootstrap.py index ab6aff2b..fe56ab03 100644 --- a/scripts/scripts_cf14.0/cloudfile_bootstrap.py +++ b/scripts/scripts_cf14.0/cloudfile_bootstrap.py @@ -221,7 +221,7 @@ def write_cloudfile_settings(): body += _settings_block_sso() # V2 enterprise org/role catalog worker interval; one low-frequency task. # Malformed values fail startup through the Hub's bounded setting gate. - body += 'CF_DIRECTORY_SYNC_INTERVAL_SECONDS = %r\\n' % int( + body += 'CF_DIRECTORY_SYNC_INTERVAL_SECONDS = %r\n' % int( get_conf('CF_DIRECTORY_SYNC_INTERVAL_SECONDS', '600')) # EAP may use the existing OAuth/SSO bridge with the legacy full # group-synchronizer disabled. When the new one-shot administrator sync is From 9d05598c52603dd2c7f8c98d764c97dc64eee85a Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:05:05 +0900 Subject: [PATCH 5/7] test: assert existing worker schedules directory sync even with Meilisearch --- tests/test_worker_lifecycle.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_worker_lifecycle.py b/tests/test_worker_lifecycle.py index 63d513e9..11e7cd9a 100644 --- a/tests/test_worker_lifecycle.py +++ b/tests/test_worker_lifecycle.py @@ -1,7 +1,7 @@ """The cf-worker service must not restart forever after a clean idle exit. `manage.py cf_worker` exits 0 when no capability registered a periodic task, and -the V2 runtime registers none today. With `restart: unless-stopped` that clean +the standalone V2 runtime registers none without a Directory. With `restart: unless-stopped` that clean exit becomes an endless restart loop (observed ExitCode=0, RestartCount 455+ on minio157), so the policy must be `on-failure`. @@ -70,6 +70,13 @@ def test_worker_runs_the_meilisearch_indexer_when_it_is_the_provider(self): self.assertIn("cloudfile_ext.v2.search.worker", command) self.assertIn("/shared/search-v2-checkpoint.json", command) + def test_meili_mode_keeps_directory_catalog_scheduling(self): + command = "\\n".join(self.block) + self.assertIn('CF_SERVICE_SSO_DIRECTORY_URL', command) + self.assertIn('periodic_pid=$!', command) + self.assertIn('wait -n "${periodic_pid}" "${search_pid}"', command) + self.assertIn('CF_DIRECTORY_SYNC_INTERVAL_SECONDS: ${CF_DIRECTORY_SYNC_INTERVAL_SECONDS:-600}', command) + def test_worker_stays_in_the_default_stack(self): self.assertEqual([], service_values(self.block, "profiles")) From 1cafb80532287d90a738b5d18a506d563e690c3b Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:05:09 +0900 Subject: [PATCH 6/7] docs: expose optional ten-minute org/role catalog sync cadence --- deploy/compose/.env.enterprise-sso.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/compose/.env.enterprise-sso.example b/deploy/compose/.env.enterprise-sso.example index c864c6aa..c3476961 100644 --- a/deploy/compose/.env.enterprise-sso.example +++ b/deploy/compose/.env.enterprise-sso.example @@ -14,3 +14,7 @@ CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED=false # Optional: require one known business administrator account in the signed roster. # This is a health assertion only; it never grants privileges by username alone. CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT= + +# Enterprise org/role catalog sync (existing cf-worker, every 10 minutes). +# Does not create users or refresh GroupUser during ordinary permission checks. +CF_DIRECTORY_SYNC_INTERVAL_SECONDS=600 From 3aff241eea823cd4f73139edd05bcc6fa58e72ac Mon Sep 17 00:00:00 2001 From: servicememeai Date: Sun, 25 Oct 2026 15:06:00 +0900 Subject: [PATCH 7/7] fix(compose): keep Python wrapper and command on same folded YAML line --- deploy/compose/docker-compose.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index 05b8c251..53a6af3d 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -414,21 +414,17 @@ services: if [ "$${CF_PROVIDER_SEARCH:-}" = "meilisearch" ]; then export DJANGO_SETTINGS_MODULE=seahub.settings; if [ -n "$${CF_SERVICE_SSO_DIRECTORY_URL:-}" ]; then - /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker & + /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker & periodic_pid=$$!; - /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json & + /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json & search_pid=$$!; trap 'kill "$${periodic_pid}" "$${search_pid}" 2>/dev/null || true' EXIT TERM INT; wait -n "$${periodic_pid}" "$${search_pid}"; exit 1; fi; - exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; + exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 -m cloudfile_ext.v2.search.worker --checkpoint /shared/search-v2-checkpoint.json; fi; - exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env - python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker + exec /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub.sh python-env python3 /opt/seafile/$${SEAFILE_SERVER}-$${SEAFILE_VERSION}/seahub/manage.py cf_worker volumes: - ./data/seafile:/shared environment: