Skip to content

Commit d66a288

Browse files
feat: support directory references in <filepath@store> (fixes #1410)
Previously is_dir was hardcoded to False in FilepathCodec.encode(), and StorageBackend.exists() used Path.is_file() which returned False for directories. Together these caused directory paths to fail the existence check and never set is_dir correctly. Changes: - storage.py: StorageBackend.exists() now uses Path.exists() so directories pass the check; add isdir() method for both local and remote (fsspec) backends. - filepath.py: encode() calls backend.isdir() to detect directories dynamically; size is set to None for directories. - objectref.py: _verify_folder() returns True (unverified-but-valid) when no manifest is present, rather than raising IntegrityError. Directories stored without a manifest are accepted. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 34acbbe commit d66a288

File tree

3 files changed

+34
-8
lines changed

3 files changed

+34
-8
lines changed

src/datajoint/builtin_codecs/filepath.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,22 @@ def encode(self, value: Any, *, key: dict | None = None, store_name: str | None
145145
if not backend.exists(path):
146146
raise FileNotFoundError(f"File not found in store '{store_name or 'default'}': {path}")
147147

148-
# Get file info
149-
try:
150-
size = backend.size(path)
151-
except Exception:
152-
size = None
148+
# Detect whether the path is a directory or a file
149+
is_dir = backend.isdir(path)
150+
151+
# Get file size (not applicable for directories)
152+
size = None
153+
if not is_dir:
154+
try:
155+
size = backend.size(path)
156+
except Exception:
157+
pass
153158

154159
return {
155160
"path": path,
156161
"store": store_name,
157162
"size": size,
158-
"is_dir": False,
163+
"is_dir": is_dir,
159164
"timestamp": datetime.now(timezone.utc).isoformat(),
160165
}
161166

src/datajoint/objectref.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ def _verify_folder(self) -> bool:
379379
manifest_path = f"{self.path}.manifest.json"
380380

381381
if not self._backend.exists(manifest_path):
382-
raise IntegrityError(f"Manifest file missing: {manifest_path}")
382+
# Directory was stored without a manifest — treat as unverified but valid
383+
return True
383384

384385
# Read manifest
385386
manifest_data = self._backend.get_buffer(manifest_path)

src/datajoint/storage.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,10 +571,30 @@ def exists(self, remote_path: str | PurePosixPath) -> bool:
571571
logger.debug(f"exists: {self.protocol}:{full_path}")
572572

573573
if self.protocol == "file":
574-
return Path(full_path).is_file()
574+
return Path(full_path).exists()
575575
else:
576576
return self.fs.exists(full_path)
577577

578+
def isdir(self, remote_path: str | PurePosixPath) -> bool:
579+
"""
580+
Check if a path refers to a directory in storage.
581+
582+
Parameters
583+
----------
584+
remote_path : str or PurePosixPath
585+
Path in storage.
586+
587+
Returns
588+
-------
589+
bool
590+
True if the path is a directory.
591+
"""
592+
full_path = self._full_path(remote_path)
593+
if self.protocol == "file":
594+
return Path(full_path).is_dir()
595+
else:
596+
return self.fs.isdir(full_path)
597+
578598
def remove(self, remote_path: str | PurePosixPath) -> None:
579599
"""
580600
Remove a file from storage.

0 commit comments

Comments
 (0)