1+ """Move and rename assets in DANDI Archive.
2+
3+ This module provides functionality for moving and renaming assets both
4+ locally and remotely in DANDI Archive instances. Features include:
5+ - Local file reorganization
6+ - Remote asset path changes
7+ - Combined local and remote moves
8+ - Conflict resolution (skip, overwrite, error)
9+ - Validation of move operations
10+ """
11+
112from __future__ import annotations
213
314from abc import ABC , abstractmethod
1324from typing import NewType
1425
1526from . import get_logger
16- from .consts import DandiInstance
27+ from .consts import DandiInstance , dandiset_metadata_file
1728from .dandiapi import DandiAPIClient , RemoteAsset , RemoteDandiset
1829from .dandiarchive import DandisetURL , parse_dandi_url
1930from .dandiset import Dandiset
@@ -233,7 +244,11 @@ def resolve(self, path: str) -> tuple[AssetPath, bool]:
233244 posixpath .normpath (posixpath .join (self .subpath .as_posix (), path ))
234245 )
235246 if p .parts and p .parts [0 ] == os .pardir :
236- raise ValueError (f"{ path !r} is outside of Dandiset" )
247+ raise ValueError (
248+ f"{ path !r} is outside of Dandiset. "
249+ "Paths cannot use '..' to navigate above the Dandiset root. "
250+ "All assets must remain within the Dandiset directory structure."
251+ )
237252 return (AssetPath (str (p )), path .endswith ("/" ))
238253
239254 def calculate_moves (
@@ -472,11 +487,17 @@ def get_path(self, path: str, is_src: bool = True) -> File | Folder:
472487 rpath , needs_dir = self .resolve (path )
473488 p = self .dandiset_path / rpath
474489 if not os .path .lexists (p ):
475- raise NotFoundError (f"No asset at local path { path !r} " )
490+ raise NotFoundError (
491+ f"No asset at local path { path !r} . "
492+ "Verify the path is correct and the file exists locally."
493+ )
476494 if p .is_dir ():
477495 if is_src :
478496 if p == self .dandiset_path / self .subpath :
479- raise ValueError ("Cannot move current working directory" )
497+ raise ValueError (
498+ "Cannot move current working directory. "
499+ "Change to a different directory before moving this location."
500+ )
480501 files = [
481502 df .filepath .relative_to (p ).as_posix ()
482503 for df in find_dandi_files (
@@ -488,7 +509,10 @@ def get_path(self, path: str, is_src: bool = True) -> File | Folder:
488509 files = []
489510 return Folder (rpath , files )
490511 elif needs_dir :
491- raise ValueError (f"Local path { path !r} is a file" )
512+ raise ValueError (
513+ f"Local path { path !r} is a file but a directory was expected. "
514+ "Use a path ending with '/' for directories."
515+ )
492516 else :
493517 return File (rpath )
494518
@@ -612,7 +636,10 @@ def get_path(self, path: str, is_src: bool = True) -> File | Folder:
612636 file_found = False
613637 if rpath == self .subpath .as_posix ():
614638 if is_src :
615- raise ValueError ("Cannot move current working directory" )
639+ raise ValueError (
640+ "Cannot move current working directory. "
641+ "Change to a different directory before moving this location."
642+ )
616643 else :
617644 return Folder (rpath , [])
618645 for p in self .assets .keys ():
@@ -629,7 +656,10 @@ def get_path(self, path: str, is_src: bool = True) -> File | Folder:
629656 if relcontents :
630657 return Folder (rpath , relcontents )
631658 if needs_dir and file_found :
632- raise ValueError (f"Remote path { path !r} is a file" )
659+ raise ValueError (
660+ f"Remote path { path !r} is a file but a directory was expected. "
661+ "Use a path ending with '/' for directories."
662+ )
633663 elif (
634664 not needs_dir
635665 and not is_src
@@ -641,7 +671,11 @@ def get_path(self, path: str, is_src: bool = True) -> File | Folder:
641671 # remote directory.
642672 return Folder (rpath , [])
643673 else :
644- raise NotFoundError (f"No asset at remote path { path !r} " )
674+ raise NotFoundError (
675+ f"No asset at remote path { path !r} . "
676+ "Verify the path is correct and the asset exists on the server. "
677+ "Use 'dandi ls' to list available assets."
678+ )
645679
646680 def is_dir (self , path : AssetPath ) -> bool :
647681 """Returns true if the given path points to a directory"""
@@ -891,7 +925,11 @@ def find_dandiset_and_subpath(path: Path) -> tuple[Dandiset, Path]:
891925 path = path .absolute ()
892926 ds = Dandiset .find (path )
893927 if ds is None :
894- raise ValueError (f"{ path } : not a Dandiset" )
928+ raise ValueError (
929+ f"{ path } : not a Dandiset. "
930+ f"The directory does not contain a '{ dandiset_metadata_file } ' file. "
931+ "Use 'dandi download' to download a dandiset first."
932+ )
895933 return (ds , path .relative_to (ds .path ))
896934
897935
0 commit comments