diff --git a/antarest/core/config.py b/antarest/core/config.py index 1a93131a0a..db14a87c50 100644 --- a/antarest/core/config.py +++ b/antarest/core/config.py @@ -165,6 +165,8 @@ def from_dict(cls, data: JSON) -> "StorageConfig": if "workspaces" in data else defaults.workspaces ) + + cls._validate_workspaces(data, workspaces) return cls( matrixstore=Path(data["matrixstore"]) if "matrixstore" in data else defaults.matrixstore, archive_dir=Path(data["archive_dir"]) if "archive_dir" in data else defaults.archive_dir, @@ -191,6 +193,19 @@ def from_dict(cls, data: JSON) -> "StorageConfig": ), ) + @classmethod + def _validate_workspaces(cls, config_as_json: JSON, workspaces: Dict[str, WorkspaceConfig]) -> None: + """ + Validate that no two workspaces have overlapping paths. + """ + workspace_name_by_path = [(config.path, name) for name, config in workspaces.items()] + for path, name in workspace_name_by_path: + for path2, name2 in workspace_name_by_path: + if name != name2 and path.is_relative_to(path2): + raise ValueError( + f"Overlapping workspace paths found: '{name}' and '{name2}' '{path}' is relative to '{path2}' " + ) + @dataclass(frozen=True) class NbCoresConfig: diff --git a/antarest/study/service.py b/antarest/study/service.py index 31bb80fa1f..9271e0dbdb 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -954,16 +954,17 @@ def sync_studies_on_disk( all_studies = [raw_study for raw_study in all_studies if directory in Path(raw_study.path).parents] else: all_studies = [raw_study for raw_study in all_studies if directory == Path(raw_study.path).parent] - studies_by_path = {study.path: study for study in all_studies} + studies_by_path_workspace = {(study.workspace, study.path): study for study in all_studies} # delete orphan studies on database - paths = [str(f.path) for f in folders] + # key should be workspace, path to sync correctly studies with same path in different workspace + workspace_paths = [(f.workspace, str(f.path)) for f in folders] for study in all_studies: if ( isinstance(study, RawStudy) and not study.archived - and (study.workspace != DEFAULT_WORKSPACE_NAME and study.path not in paths) + and (study.workspace != DEFAULT_WORKSPACE_NAME and (study.workspace, study.path) not in workspace_paths) ): if not study.missing: logger.info( @@ -990,11 +991,12 @@ def sync_studies_on_disk( self.repository.delete(study.id) # Add new studies - study_paths = [study.path for study in all_studies if study.missing is None] + study_paths = [(study.workspace, study.path) for study in all_studies if study.missing is None] missing_studies = {study.path: study for study in all_studies if study.missing is not None} for folder in folders: study_path = str(folder.path) - if study_path not in study_paths: + workspace = folder.workspace + if (workspace, study_path) not in study_paths: try: if study_path not in missing_studies.keys(): base_path = self.config.storage.workspaces[folder.workspace].path @@ -1004,7 +1006,7 @@ def sync_studies_on_disk( name=folder.path.name, path=study_path, folder=str(dir_name), - workspace=folder.workspace, + workspace=workspace, owner=None, groups=folder.groups, public_mode=PublicMode.FULL if len(folder.groups) == 0 else PublicMode.NONE, @@ -1039,8 +1041,8 @@ def sync_studies_on_disk( ) except Exception as e: logger.error(f"Failed to add study {folder.path}", exc_info=e) - elif directory and study_path in studies_by_path: - existing_study = studies_by_path[study_path] + elif directory and (workspace, study_path) in studies_by_path_workspace: + existing_study = studies_by_path_workspace[(workspace, study_path)] if self.storage_service.raw_study_service.update_name_and_version_from_raw_meta(existing_study): self.repository.save(existing_study) diff --git a/tests/storage/test_config.py b/tests/storage/test_config.py new file mode 100644 index 0000000000..9e7dc8ec47 --- /dev/null +++ b/tests/storage/test_config.py @@ -0,0 +1,103 @@ +from pathlib import Path + +import pytest + +from antarest.core.config import InternalMatrixFormat, StorageConfig + + +@pytest.fixture +def storage_config_default(): + return { + "matrixstore": "./custom_matrixstore", + "archive_dir": "./custom_archives", + "tmp_dir": "./custom_tmp", + "allow_deletion": True, + "watcher_lock": False, + "watcher_lock_delay": 20, + "download_default_expiration_timeout_minutes": 2880, + "matrix_gc_sleeping_time": 7200, + "matrix_gc_dry_run": True, + "auto_archive_threshold_days": 120, + "auto_archive_dry_run": True, + "auto_archive_sleeping_time": 7200, + "auto_archive_max_parallel": 10, + "snapshot_retention_days": 14, + "matrixstore_format": "tsv", + } + + +def test_storage_config_from_dict(storage_config_default): + data = { + **storage_config_default, + "workspaces": { + "workspace1": { + "path": "./workspace1", + }, + "workspace2": { + "path": "./workspace2", + }, + }, + } + + config = StorageConfig.from_dict(data) + + assert config.matrixstore == Path("./custom_matrixstore") + assert config.archive_dir == Path("./custom_archives") + assert config.tmp_dir == Path("./custom_tmp") + assert config.workspaces["workspace1"].path == Path("./workspace1") + assert config.workspaces["workspace2"].path == Path("./workspace2") + assert config.allow_deletion is True + assert config.watcher_lock is False + assert config.watcher_lock_delay == 20 + assert config.download_default_expiration_timeout_minutes == 2880 + assert config.matrix_gc_sleeping_time == 7200 + assert config.matrix_gc_dry_run is True + assert config.auto_archive_threshold_days == 120 + assert config.auto_archive_dry_run is True + assert config.auto_archive_sleeping_time == 7200 + assert config.auto_archive_max_parallel == 10 + assert config.snapshot_retention_days == 14 + assert config.matrixstore_format == InternalMatrixFormat.TSV + + +def test_storage_config_from_dict_validiation_errors(storage_config_default): + data = { + **storage_config_default, + "workspaces": { + "workspace1": { + "path": "./a/workspace1", + }, + "workspace2": { + "path": "./a/", + }, + }, + } + + with pytest.raises(ValueError): + StorageConfig.from_dict(data) + + data = { + **storage_config_default, + "workspaces": { + "workspace1": { + "path": "./a/", + }, + "workspace2": { + "path": "./a/workspace2", + }, + }, + } + + with pytest.raises(ValueError): + StorageConfig.from_dict(data) + + data = { + **storage_config_default, + "workspaces": { + "workspace1": {"path": "./a/", "some_other_config": "value1"}, + "workspace2": {"path": "./a/", "some_other_config": "value2"}, + }, + } + + with pytest.raises(ValueError): + StorageConfig.from_dict(data) diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index 06a7d45b0f..bd860d1ba9 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -259,8 +259,8 @@ def test_study_listing(db_session: Session) -> None: @pytest.mark.unit_test def test_sync_studies_from_disk() -> None: now = datetime.utcnow() - ma = RawStudy(id="a", path="a") - fa = StudyFolder(path=Path("a"), workspace="", groups=[]) + ma = RawStudy(id="a", path="a", workspace="workspace1") + fa = StudyFolder(path=Path("a"), workspace="workspace1", groups=[]) mb = RawStudy(id="b", path="b") mc = RawStudy( id="c",