Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 94 additions & 45 deletions patches/seafobj/automatic-local-storage.patch
Original file line number Diff line number Diff line change
@@ -1,50 +1,6 @@
diff --git a/seafobj/objstore_factory.py b/seafobj/objstore_factory.py
index c7f4219..f960e44 100644
--- a/seafobj/objstore_factory.py
+++ b/seafobj/objstore_factory.py
@@ -4,7 +4,7 @@ import binascii
import logging
import json

-from sqlalchemy import select
+from sqlalchemy import select, text

from seafobj.exceptions import InvalidConfigError
from seafobj.backends.filesystem import SeafObjStoreFS
@@ -638,7 +638,16 @@ class SeafObjStoreFactory(object):
except KeyError:
raise RuntimeError('unknown obj_type ' + obj_type)

+ # Construct the lazy mapping before managers capture this dictionary;
+ # patching it at Django AppConfig.ready() would be too late for seafobj.
+ from seafobj.auto_local import AutoLocalStores, auto_template
+ parent = auto_template(self.seafile_cfg.multi_backend_cfg)
+ if parent is not None:
+ self.obj_stores[obj_type] = AutoLocalStores(parent, obj_type, self.seafile_cfg.crypto)
+
for bend in self.seafile_cfg.multi_backend_cfg:
+ if bend.get('is_auto') is True:
+ continue
storage_id = bend['storage_id']

crypto = self.seafile_cfg.crypto
@@ -715,8 +724,12 @@ def get_repo_storage_id(repo_id):
RepoStorageId = Base.classes.RepoStorageId
storage_id = None
session = scoped_session(objstore_factory.session)
- r = session.scalars(select(RepoStorageId).where(RepoStorageId.repo_id == repo_id).limit(1)).first()
- storage_id = r.storage_id if r else None
+ # Virtual libraries share origin objects; match the native C/Go query
+ # rather than silently sending a virtual UUID to the legacy backend.
+ storage_id = session.scalar(text(
+ 'SELECT storage_id FROM RepoStorageId WHERE repo_id = COALESCE('
+ '(SELECT origin_repo FROM VirtualRepo WHERE repo_id = :repo), :repo) LIMIT 1'),
+ {'repo': repo_id})
repo_storage_id[repo_id] = storage_id
session.remove()
return storage_id
diff --git a/seafobj/auto_local.py b/seafobj/auto_local.py
new file mode 100644
index 0000000..f5d9233
--- /dev/null
+++ b/seafobj/auto_local.py
@@ -0,0 +1,54 @@
Expand Down Expand Up @@ -102,3 +58,96 @@ new file mode 100644
+ if item.get(kind) != {'backend': 'fs', 'dir': root}:
+ raise ValueError('Automatic local template requires one filesystem root')
+ return root
diff --git a/seafobj/db.py b/seafobj/db.py
index d64d743..377ae73 100644
--- a/seafobj/db.py
+++ b/seafobj/db.py
@@ -33,7 +33,12 @@ def load_db_url_from_env():
def create_engine_from_conf(config):
need_connection_pool_fix = True

- if config.has_section('database'):
+ # CloudFile containers pass complete MySQL facts in the environment and
+ # keep only connection_charset in seafile.conf. Prefer the environment URL
+ # before parsing a [database] section that may legitimately be partial.
+ db_url = load_db_url_from_env()
+
+ if db_url is None and config.has_section('database'):
backend = config.get('database', 'type')

if backend == 'mysql':
diff --git a/seafobj/objstore_factory.py b/seafobj/objstore_factory.py
index c7f4219..c8de5f7 100644
--- a/seafobj/objstore_factory.py
+++ b/seafobj/objstore_factory.py
@@ -4,7 +4,7 @@ import binascii
import logging
import json

-from sqlalchemy import select
+from sqlalchemy import select, text

from seafobj.exceptions import InvalidConfigError
from seafobj.backends.filesystem import SeafObjStoreFS
@@ -440,6 +440,29 @@ class SeafileConfig(object):
# Get storage backend from seafile.conf
cfg_file_parser = self.cfg_file_parser

+ # CloudFile V2 automatic local root: every routed library keeps its own
+ # key directory while original CE libraries keep the native tree.
+ # Reuse the reviewed multiple-backend machinery with one synthetic auto
+ # template and a native default, so every object manager resolves the
+ # physical location through RepoStorageId exactly like C and Go do.
+ auto_root = ''
+ if cfg_file_parser.has_option('cloudfile_v2', 'auto_local_root'):
+ auto_root = (cfg_file_parser.get('cloudfile_v2', 'auto_local_root') or '').strip()
+ if auto_root:
+ def _fs_backend(root):
+ return {'backend': 'fs', 'dir': root}
+ native_root = os.path.dirname(self.get_seafile_storage_dir())
+ self.storage_type = 'multiple'
+ self.multi_backend_cfg = [
+ {'storage_id': 'auto-local', 'is_auto': True, 'is_default': False,
+ 'commits': _fs_backend(auto_root), 'fs': _fs_backend(auto_root),
+ 'blocks': _fs_backend(auto_root)},
+ {'storage_id': 'native', 'is_default': True,
+ 'commits': _fs_backend(native_root), 'fs': _fs_backend(native_root),
+ 'blocks': _fs_backend(native_root)},
+ ]
+ return
+
enable_storage_classes = False
if self.storage_type == 'multiple':
enable_storage_classes = True
@@ -638,7 +661,16 @@ class SeafObjStoreFactory(object):
except KeyError:
raise RuntimeError('unknown obj_type ' + obj_type)

+ # Construct the lazy mapping before managers capture this dictionary;
+ # patching it at Django AppConfig.ready() would be too late for seafobj.
+ from seafobj.auto_local import AutoLocalStores, auto_template
+ parent = auto_template(self.seafile_cfg.multi_backend_cfg)
+ if parent is not None:
+ self.obj_stores[obj_type] = AutoLocalStores(parent, obj_type, self.seafile_cfg.crypto)
+
for bend in self.seafile_cfg.multi_backend_cfg:
+ if bend.get('is_auto') is True:
+ continue
storage_id = bend['storage_id']

crypto = self.seafile_cfg.crypto
@@ -715,8 +747,12 @@ def get_repo_storage_id(repo_id):
RepoStorageId = Base.classes.RepoStorageId
storage_id = None
session = scoped_session(objstore_factory.session)
- r = session.scalars(select(RepoStorageId).where(RepoStorageId.repo_id == repo_id).limit(1)).first()
- storage_id = r.storage_id if r else None
+ # Virtual libraries share origin objects; match the native C/Go query
+ # rather than silently sending a virtual UUID to the legacy backend.
+ storage_id = session.scalar(text(
+ 'SELECT storage_id FROM RepoStorageId WHERE repo_id = COALESCE('
+ '(SELECT origin_repo FROM VirtualRepo WHERE repo_id = :repo), :repo) LIMIT 1'),
+ {'repo': repo_id})
repo_storage_id[repo_id] = storage_id
session.remove()
return storage_id
2 changes: 1 addition & 1 deletion scripts/scripts_cf14.0/cloudfile_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ def write_cloudfile_seafile_conf():
conf_path = join(topdir, 'conf', 'seafile.conf')
fresh = fresh_install()
if auto_storage_enabled(conf_path, os.environ, fresh=fresh):
configure_auto_storage(conf_path, ''.join(lines), installdir)
configure_auto_storage(conf_path, ''.join(lines), installdir, fresh=fresh)
return

# Keep the S3 configuration in the same restart-safe generated block as
Expand Down
16 changes: 15 additions & 1 deletion scripts/scripts_cf14.0/library_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def _render_auto(original, generated_body):
return ''.join(out).rstrip() + '\n\n' + BEGIN + '\n' + body + END + '\n'


def configure_auto_storage(conf, generated_body, installdir, environment=None):
def configure_auto_storage(conf, generated_body, installdir, environment=None, *, fresh=False):
"""Preflight old routing, register supported libraries, then replace config.

Neither a failed preflight nor a failed apply changes seafile.conf. Apply
Expand All @@ -129,6 +129,20 @@ def configure_auto_storage(conf, generated_body, installdir, environment=None):
final = _render_auto(original, generated_body)
check = ConfigParser(interpolation=None)
check.read_string(final)

# A fresh installation has no old repositories to register. Existing V2
# already has persistent native RepoStorageId routes; do not rescan and
# rewrite every library on each restart. Preflight is only for an explicit
# transition from an existing non-V2 Seafile installation.
if fresh or current_root == LIBRARY_ROOT:
if current_root == LIBRARY_ROOT and (
current.getboolean('storage', 'enable_storage_classes', fallback=False) or
any(current.get(section, 'name', fallback='filesystem') != 'filesystem'
for section in ('commit_object_backend', 'fs_object_backend', 'block_backend'))):
raise ValueError('Existing V2 storage configuration is incompatible')
_atomic_write(conf, final)
return

if not current.has_section('cloudfile_v2'):
current.add_section('cloudfile_v2')
current.set('cloudfile_v2', 'auto_local_root', LIBRARY_ROOT)
Expand Down
22 changes: 22 additions & 0 deletions tests/test_library_storage_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ def test_new_auto_uses_filesystem_and_reconciles_before_write(self):
self.assertTrue(storage.auto_storage_enabled(self.conf, {}))
self.assertEqual([], list(self.conf.parent.glob('.storage-*')))

def test_fresh_uuid_v2_install_skips_native_inventory(self):
with patch.object(storage.subprocess, 'run') as run:
storage.configure_auto_storage(self.conf, '[cloudfile]\nexample = true\n',
self.installdir, self.env, fresh=True)
run.assert_not_called()
config = ConfigParser(interpolation=None)
config.read(self.conf)
self.assertEqual(str(self.root), config['cloudfile_v2']['auto_local_root'])
self.assertEqual('false', config['storage']['enable_storage_classes'])

def test_existing_v2_restart_never_rewrites_route_registry(self):
self.configure() # explicit first transition from an existing CE config
calls_before = len(self.calls)
with patch.object(storage.subprocess, 'run') as run:
storage.configure_auto_storage(self.conf, '[cloudfile]\nexample = true\n',
self.installdir, self.env)
run.assert_not_called()
self.assertEqual(calls_before, len(self.calls))

def test_native_layout_symlinks_are_resolved_but_managed_root_is_not(self):
shared = self.top / 'shared-seafile'
shared.mkdir()
Expand All @@ -214,6 +233,9 @@ def test_explicit_transition_inventories_original_classes(self):
def test_schema_missing_and_state_drift_allow_explicit_apply(self):
for status in ('SCHEMA_MISSING', 'STATE_DRIFT'):
with self.subTest(status=status):
# Every case models a distinct explicit CE transition.
# A completed V2 restart intentionally skips reconcile.
self.conf.write_text(self.original)
modes = []
def cli(command, **kwargs):
modes.append(command[-1])
Expand Down
6 changes: 5 additions & 1 deletion tests/test_v06_default_library_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ def test_merged_new_install_compose_contract(self):
mounts = [v for v in process["volumes"] if v["target"] == parent]
self.assertEqual(1, len(mounts), "one parent mount per process")
self.assertEqual("rw", mounts[0].get("mode", "rw"))
self.assertFalse(mounts[0]["bind"]["create_host_path"])
# Compose JSON may omit explicit false from the normalized form.
# The deployment overlay must still declare it for safe mounts.
self.assertFalse(mounts[0].get("bind", {}).get("create_host_path", False))
self.assertEqual(2, (ROOT / "docker-compose.auto-library-storage.yml")
.read_text().count("create_host_path: false"))
self.assertEqual(False, str(server["environment"]["CF_ENABLE_CHECKOUT"]).lower() == "true")


Expand Down
Loading