1010from abc import ABC
1111from abc import abstractmethod
1212from typing import Any
13+ from typing import cast
1314from typing import Dict
1415from typing import List
16+ from typing import Literal
1517from typing import Optional
18+ from typing import overload
1619from typing import Union
1720
1821from .. import config
@@ -421,15 +424,36 @@ def is_dir(self, path: PathLike) -> bool:
421424 def is_file (self , path : PathLike ) -> bool :
422425 pass
423426
424- @abstractmethod
427+ @overload
425428 def listdir (
426429 self ,
427430 path : PathLike = '/' ,
428431 * ,
429432 recursive : bool = False ,
433+ return_objects : Literal [True ],
434+ ) -> List [FilesObject ]:
435+ pass
436+
437+ @overload
438+ def listdir (
439+ self ,
440+ path : PathLike = '/' ,
441+ * ,
442+ recursive : bool = False ,
443+ return_objects : Literal [False ] = False ,
430444 ) -> List [str ]:
431445 pass
432446
447+ @abstractmethod
448+ def listdir (
449+ self ,
450+ path : PathLike = '/' ,
451+ * ,
452+ recursive : bool = False ,
453+ return_objects : bool = False ,
454+ ) -> Union [List [str ], List [FilesObject ]]:
455+ pass
456+
433457 @abstractmethod
434458 def download_file (
435459 self ,
@@ -908,38 +932,78 @@ def is_file(self, path: PathLike) -> bool:
908932 return False
909933 raise
910934
911- def _listdir (self , path : PathLike , * , recursive : bool = False ) -> List [str ]:
935+ def _listdir (
936+ self , path : PathLike , * ,
937+ recursive : bool = False ,
938+ return_objects : bool = False ,
939+ ) -> List [Union [str , FilesObject ]]:
912940 """
913- Return the names of files in a directory.
941+ Return the names (or FilesObject instances) of files in a directory.
914942
915943 Parameters
916944 ----------
917945 path : Path or str
918946 Path to the folder
919947 recursive : bool, optional
920948 Should folders be listed recursively?
921-
949+ return_objects : bool, optional
950+ If True, return list of FilesObject instances. Otherwise just paths.
922951 """
923952 res = self ._manager ._get (
924953 f'files/fs/{ self ._location } /{ path } ' ,
925954 ).json ()
926955
927956 if recursive :
928- out = []
929- for item in res ['content' ] or []:
930- out .append (item ['path' ])
957+ out : List [Union [str , FilesObject ]] = []
958+ for item in res .get ('content' ) or []:
959+ if return_objects :
960+ out .append (FilesObject .from_dict (item , self ))
961+ else :
962+ out .append (item ['path' ])
931963 if item ['type' ] == 'directory' :
932- out .extend (self ._listdir (item ['path' ], recursive = recursive ))
964+ out .extend (
965+ self ._listdir (
966+ item ['path' ],
967+ recursive = recursive ,
968+ return_objects = return_objects ,
969+ ),
970+ )
933971 return out
934972
935- return [x ['path' ] for x in res ['content' ] or []]
973+ if return_objects :
974+ return [
975+ FilesObject .from_dict (x , self )
976+ for x in (res .get ('content' ) or [])
977+ ]
978+ return [x ['path' ] for x in (res .get ('content' ) or [])]
936979
980+ @overload
937981 def listdir (
938982 self ,
939983 path : PathLike = '/' ,
940984 * ,
941985 recursive : bool = False ,
986+ return_objects : Literal [True ],
987+ ) -> List [FilesObject ]:
988+ ...
989+
990+ @overload
991+ def listdir (
992+ self ,
993+ path : PathLike = '/' ,
994+ * ,
995+ recursive : bool = False ,
996+ return_objects : Literal [False ] = False ,
942997 ) -> List [str ]:
998+ ...
999+
1000+ def listdir (
1001+ self ,
1002+ path : PathLike = '/' ,
1003+ * ,
1004+ recursive : bool = False ,
1005+ return_objects : bool = False ,
1006+ ) -> Union [List [str ], List [FilesObject ]]:
9431007 """
9441008 List the files / folders at the given path.
9451009
@@ -948,22 +1012,40 @@ def listdir(
9481012 path : Path or str, optional
9491013 Path to the file location
9501014
1015+ return_objects : bool, optional
1016+ If True, return list of FilesObject instances. Otherwise just paths.
1017+
9511018 Returns
9521019 -------
953- List[str]
1020+ List[str] or List[FilesObject]
9541021
9551022 """
9561023 path = re .sub (r'^(\./|/)+' , r'' , str (path ))
9571024 path = re .sub (r'/+$' , r'' , path ) + '/'
9581025
959- if not self .is_dir (path ):
960- raise NotADirectoryError (f'path is not a directory: { path } ' )
1026+ # Validate via listing GET; if response lacks 'content', it's not a directory
1027+ try :
1028+ out = self ._listdir (path , recursive = recursive , return_objects = return_objects )
1029+ except (ManagementError , KeyError ) as exc :
1030+ # If the path doesn't exist or isn't a directory, _listdir will fail
1031+ raise NotADirectoryError (f'path is not a directory: { path } ' ) from exc
9611032
962- out = self ._listdir (path , recursive = recursive )
9631033 if path != '/' :
9641034 path_n = len (path .split ('/' )) - 1
965- out = ['/' .join (x .split ('/' )[path_n :]) for x in out ]
966- return out
1035+ if return_objects :
1036+ result : List [FilesObject ] = []
1037+ for item in out :
1038+ if isinstance (item , FilesObject ):
1039+ rel = '/' .join (item .path .split ('/' )[path_n :])
1040+ item .path = rel
1041+ result .append (item )
1042+ return result
1043+ return ['/' .join (str (x ).split ('/' )[path_n :]) for x in out ]
1044+
1045+ # _listdir guarantees homogeneous type based on return_objects
1046+ if return_objects :
1047+ return cast (List [FilesObject ], out )
1048+ return cast (List [str ], out )
9671049
9681050 def download_file (
9691051 self ,
@@ -992,10 +1074,49 @@ def download_file(
9921074 bytes or str - ``local_path`` is None
9931075 None - ``local_path`` is a Path or str
9941076
1077+ """
1078+ return self ._download_file (
1079+ path ,
1080+ local_path = local_path ,
1081+ overwrite = overwrite ,
1082+ encoding = encoding ,
1083+ _skip_dir_check = False ,
1084+ )
1085+
1086+ def _download_file (
1087+ self ,
1088+ path : PathLike ,
1089+ local_path : Optional [PathLike ] = None ,
1090+ * ,
1091+ overwrite : bool = False ,
1092+ encoding : Optional [str ] = None ,
1093+ _skip_dir_check : bool = False ,
1094+ ) -> Optional [Union [bytes , str ]]:
1095+ """
1096+ Internal method to download the content of a file path.
1097+
1098+ Parameters
1099+ ----------
1100+ path : Path or str
1101+ Path to the file
1102+ local_path : Path or str
1103+ Path to local file target location
1104+ overwrite : bool, optional
1105+ Should an existing file be overwritten if it exists?
1106+ encoding : str, optional
1107+ Encoding used to convert the resulting data
1108+ _skip_dir_check : bool, optional
1109+ Skip the directory check (internal use only)
1110+
1111+ Returns
1112+ -------
1113+ bytes or str - ``local_path`` is None
1114+ None - ``local_path`` is a Path or str
1115+
9951116 """
9961117 if local_path is not None and not overwrite and os .path .exists (local_path ):
9971118 raise OSError ('target file already exists; use overwrite=True to replace' )
998- if self .is_dir (path ):
1119+ if not _skip_dir_check and self .is_dir (path ):
9991120 raise IsADirectoryError (f'file path is a directory: { path } ' )
10001121
10011122 out = self ._manager ._get (
@@ -1036,17 +1157,27 @@ def download_folder(
10361157 if local_path is not None and not overwrite and os .path .exists (local_path ):
10371158 raise OSError ('target path already exists; use overwrite=True to replace' )
10381159
1039- if not self .is_dir (path ):
1040- raise NotADirectoryError (f'path is not a directory: { path } ' )
1041-
1042- files = self .listdir (path , recursive = True )
1043- for f in files :
1044- remote_path = os .path .join (path , f )
1045- if self .is_dir (remote_path ):
1160+ # listdir validates directory; no extra info call needed
1161+ entries = self .listdir (path , recursive = True , return_objects = True )
1162+ for entry in entries :
1163+ # Each entry is a FilesObject with path relative to root and type
1164+ if not isinstance (entry , FilesObject ): # defensive: skip unexpected
1165+ continue
1166+ rel_path = entry .path
1167+ if entry .type == 'directory' :
1168+ # Ensure local directory exists; no remote call needed
1169+ target_dir = os .path .normpath (os .path .join (local_path , rel_path ))
1170+ os .makedirs (target_dir , exist_ok = True )
10461171 continue
1047- target = os .path .normpath (os .path .join (local_path , f ))
1048- os .makedirs (os .path .dirname (target ), exist_ok = True )
1049- self .download_file (remote_path , target , overwrite = overwrite )
1172+ remote_path = os .path .join (path , rel_path )
1173+ target_file = os .path .normpath (
1174+ os .path .join (local_path , rel_path ),
1175+ )
1176+ os .makedirs (os .path .dirname (target_file ), exist_ok = True )
1177+ self ._download_file (
1178+ remote_path , target_file ,
1179+ overwrite = overwrite , _skip_dir_check = True ,
1180+ )
10501181
10511182 def remove (self , path : PathLike ) -> None :
10521183 """
0 commit comments