33"""
44
55import builtins
6+ import os
67from dataclasses import dataclass
78from datetime import datetime
89from email .utils import parsedate_to_datetime
910from io import BytesIO
1011from json import dumps , loads
11- from os import path as p
1212from pathlib import Path
1313from random import choice
1414from string import ascii_lowercase , digits
2424
2525
2626class FsNodeInfo (TypedDict ):
27+ """Extra FS object attributes from Nextcloud"""
28+
2729 nc_id : str
30+ """Nextcloud instance ID."""
2831 fileid : int
32+ """Object file ID."""
2933 etag : str
3034 size : int
3135 content_length : int
@@ -36,6 +40,19 @@ class FsNodeInfo(TypedDict):
3640
3741@dataclass
3842class FsNode :
43+ """A class that represents a Nextcloud file object.
44+
45+ Acceptable itself as a ``path`` parameter for the most file APIs."""
46+
47+ user : str
48+ """The username of the object. May be different from the owner of the object if it is shared."""
49+
50+ path : str
51+ """Path to the object. Does not include the username, but includes the object name."""
52+
53+ name : str
54+ """last ``pathname`` component."""
55+
3956 def __init__ (self , user : str , path : str , name : str , ** kwargs ):
4057 self .user = user
4158 self .path = path
@@ -66,10 +83,14 @@ def last_modified(self, value: Union[str, datetime]):
6683
6784 @property
6885 def is_dir (self ) -> bool :
86+ """Returns ``True`` for the directories, ``False`` otherwise."""
87+
6988 return self .path .endswith ("/" )
7089
7190 @property
7291 def full_path (self ) -> str :
92+ """Full path including username."""
93+
7394 return f"{ self .user } /{ self .path .lstrip ('/' )} " if self .user else self .path
7495
7596 def __str__ (self ):
@@ -117,20 +138,40 @@ class FilesAPI:
117138 def __init__ (self , session : NcSessionBasic ):
118139 self ._session = session
119140
120- def listdir (self , path = "" , exclude_self = True ) -> list [FsNode ]:
141+ def listdir (self , path : Union [str , FsNode ] = "" , exclude_self = True ) -> list [FsNode ]:
142+ """Returns a list of all entries in the specified directory.
143+
144+ :param path: Path to the directory to get the list.
145+ :param exclude_self: Boolean value indicating whether the `path` itself should be excluded from the list or not.
146+ Default = **True**.
147+ """
148+
121149 properties = PROPFIND_PROPERTIES
150+ path = path .path if isinstance (path , FsNode ) else path
122151 return self ._listdir (self ._session .user , path , properties = properties , exclude_self = exclude_self )
123152
124153 def by_id (self , fileid : int ) -> Optional [FsNode ]:
154+ """Returns :py:class:`FsNode` by fileid if any."""
155+
125156 result = self .find (req = ["eq" , "fileid" , fileid ])
126157 return result [0 ] if result else None
127158
128159 def by_path (self , path : str ) -> Optional [FsNode ]:
160+ """Returns :py:class:`FsNode` by exact path if any."""
161+
129162 result = self .listdir (path , exclude_self = False )
130163 return result [0 ] if result else None
131164
132- def find (self , req : list , path = "" , depth = - 1 ) -> list [FsNode ]:
165+ def find (self , req : list , path : Union [str , FsNode ] = "" , depth = - 1 ) -> list [FsNode ]:
166+ """Searches a directory for a file or subdirectory with a name.
167+
168+ :param req: list of conditions to search for. Detailed description here...
169+ :param path: Path where to search from. Default = **""**.
170+ :param depth: In how many levels of subdirectories to search. Default = **-1**.
171+ """
172+
133173 # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
174+ path = path .path if isinstance (path , FsNode ) else path
134175 root = ElementTree .Element (
135176 "d:searchrequest" ,
136177 attrib = {"xmlns:d" : "DAV:" , "xmlns:oc" : "http://owncloud.org/ns" , "xmlns:nc" : "http://nextcloud.org/ns" },
@@ -158,25 +199,27 @@ def find(self, req: list, path="", depth=-1) -> list[FsNode]:
158199 request_info = f"find: { self ._session .user } , { req } , { path } , { depth } "
159200 return self ._lf_parse_webdav_records (webdav_response , self ._session .user , request_info )
160201
161- def download (self , path : str ) -> bytes :
202+ def download (self , path : Union [ str , FsNode ] ) -> bytes :
162203 """Downloads and returns the content of a file.
163204
164- :param path: Path to a file to download relative to root directory of the user .
205+ :param path: Path to download file .
165206 """
166207
208+ path = path .path if isinstance (path , FsNode ) else path
167209 response = self ._session .dav ("GET" , self ._dav_get_obj_path (self ._session .user , path ))
168210 check_error (response .status_code , f"download: user={ self ._session .user } , path={ path } " )
169211 return response .content
170212
171- def download2stream (self , path : str , fp , ** kwargs ) -> None :
213+ def download2stream (self , path : Union [ str , FsNode ] , fp , ** kwargs ) -> None :
172214 """Downloads file to the given `fp` object.
173215
174- :param path: Path to a file to download relative to root directory of the user .
216+ :param path: Path to download file .
175217 :param fp: A filename (string), pathlib.Path object or a file object.
176218 The object must implement the ``file.write`` method and be able to write binary data.
177219 :param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **4Mb**
178220 """
179221
222+ path = path .path if isinstance (path , FsNode ) else path
180223 if isinstance (fp , (str , Path )):
181224 with builtins .open (fp , "wb" ) as f :
182225 self .__download2stream (path , f , ** kwargs )
@@ -185,25 +228,27 @@ def download2stream(self, path: str, fp, **kwargs) -> None:
185228 else :
186229 raise TypeError ("`fp` must be a path to file or an object with `write` method." )
187230
188- def upload (self , path : str , content : Union [bytes , str ]) -> None :
231+ def upload (self , path : Union [ str , FsNode ] , content : Union [bytes , str ]) -> None :
189232 """Creates a file with the specified content at the specified path.
190233
191- :param path: Path to a file to download relative to root directory of the user .
234+ :param path: File upload path .
192235 :param content: content to create the file. If it is a string, it will be encoded into bytes using UTF-8.
193236 """
194237
238+ path = path .path if isinstance (path , FsNode ) else path
195239 response = self ._session .dav ("PUT" , self ._dav_get_obj_path (self ._session .user , path ), data = content )
196240 check_error (response .status_code , f"upload: user={ self ._session .user } , path={ path } , size={ len (content )} " )
197241
198- def upload_stream (self , path : str , fp , ** kwargs ) -> None :
242+ def upload_stream (self , path : Union [ str , FsNode ] , fp , ** kwargs ) -> None :
199243 """Creates a file with content provided by `fp` object at the specified path.
200244
201- :param path: Path to a file to download relative to root directory of the user .
245+ :param path: File upload path .
202246 :param fp: A filename (string), pathlib.Path object or a file object.
203247 The object must implement the ``file.read`` method providing data with str or bytes type.
204248 :param kwargs: **chunk_size** an int value specifying chunk size to read. Default = **4Mb**
205249 """
206250
251+ path = path .path if isinstance (path , FsNode ) else path
207252 if isinstance (fp , (str , Path )):
208253 with builtins .open (fp , "rb" ) as f :
209254 self .__upload_stream (path , f , ** kwargs )
@@ -212,14 +257,27 @@ def upload_stream(self, path: str, fp, **kwargs) -> None:
212257 else :
213258 raise TypeError ("`fp` must be a path to file or an object with `read` method." )
214259
215- def mkdir (self , path : str ) -> None :
260+ def mkdir (self , path : Union [str , FsNode ]) -> None :
261+ """Creates a new directory.
262+
263+ :param path: The path of the directory to be created.
264+ """
265+
266+ path = path .path if isinstance (path , FsNode ) else path
216267 response = self ._session .dav ("MKCOL" , self ._dav_get_obj_path (self ._session .user , path ))
217268 check_error (response .status_code , f"mkdir: user={ self ._session .user } , path={ path } " )
218269
219- def makedirs (self , path : str , exist_ok = False ) -> None :
270+ def makedirs (self , path : Union [str , FsNode ], exist_ok = False ) -> None :
271+ """Creates a new directory and subdirectories.
272+
273+ :param path: The path of the directories to be created.
274+ :param exist_ok: Ignore error if any of pathname components already exists.
275+ """
276+
220277 _path = ""
278+ path = path .path if isinstance (path , FsNode ) else path
221279 for i in Path (path ).parts :
222- _path = p .join (_path , i )
280+ _path = os . path .join (_path , i )
223281 if not exist_ok :
224282 self .mkdir (_path )
225283 else :
@@ -229,13 +287,30 @@ def makedirs(self, path: str, exist_ok=False) -> None:
229287 if e .status_code != 405 :
230288 raise e from None
231289
232- def delete (self , path : str , not_fail = False ) -> None :
290+ def delete (self , path : Union [str , FsNode ], not_fail = False ) -> None :
291+ """Deletes a file/directory (moves to trash if trash is enabled).
292+
293+ :param path: Path to delete.
294+ :param not_fail: if set to ``True`` and object is not found, does not raise an exception.
295+ """
296+
297+ path = path .path if isinstance (path , FsNode ) else path
233298 response = self ._session .dav ("DELETE" , self ._dav_get_obj_path (self ._session .user , path ))
234299 if response .status_code == 404 and not_fail :
235300 return
236301 check_error (response .status_code , f"delete: user={ self ._session .user } , path={ path } " )
237302
238- def move (self , path_src : str , path_dest : str , overwrite = False ) -> None :
303+ def move (self , path_src : Union [str , FsNode ], path_dest : Union [str , FsNode ], overwrite = False ) -> None :
304+ """Moves an existing file or a directory.
305+
306+ :param path_src: The path of an existing file/directory.
307+ :param path_dest: The name of the new one.
308+ :param overwrite: If ``True`` and destination object already exists it gets overwritten.
309+ Default = **False**.
310+ """
311+
312+ path_src = path_src .path if isinstance (path_src , FsNode ) else path_src
313+ path_dest = path_dest .path if isinstance (path_dest , FsNode ) else path_dest
239314 dest = self ._session .cfg .dav_endpoint + self ._dav_get_obj_path (self ._session .user , path_dest )
240315 headers = {"Destination" : dest , "Overwrite" : "T" if overwrite else "F" }
241316 response = self ._session .dav (
@@ -245,7 +320,17 @@ def move(self, path_src: str, path_dest: str, overwrite=False) -> None:
245320 )
246321 check_error (response .status_code , f"move: user={ self ._session .user } , src={ path_src } , dest={ dest } , { overwrite } " )
247322
248- def copy (self , path_src : str , path_dest : str , overwrite = False ) -> None :
323+ def copy (self , path_src : Union [str , FsNode ], path_dest : Union [str , FsNode ], overwrite = False ) -> None :
324+ """Copies an existing file/directory.
325+
326+ :param path_src: The path of an existing file/directory.
327+ :param path_dest: The name of the new one.
328+ :param overwrite: If ``True`` and destination object already exists it gets overwritten.
329+ Default = **False**.
330+ """
331+
332+ path_src = path_src .path if isinstance (path_src , FsNode ) else path_src
333+ path_dest = path_dest .path if isinstance (path_dest , FsNode ) else path_dest
249334 dest = self ._session .cfg .dav_endpoint + self ._dav_get_obj_path (self ._session .user , path_dest )
250335 headers = {"Destination" : dest , "Overwrite" : "T" if overwrite else "F" }
251336 response = self ._session .dav (
@@ -256,6 +341,8 @@ def copy(self, path_src: str, path_dest: str, overwrite=False) -> None:
256341 check_error (response .status_code , f"copy: user={ self ._session .user } , src={ path_src } , dest={ dest } , { overwrite } " )
257342
258343 def listfav (self ) -> list [FsNode ]:
344+ """Returns a list of the current user's favorite files."""
345+
259346 root = ElementTree .Element (
260347 "oc:filter-files" ,
261348 attrib = {"xmlns:d" : "DAV:" , "xmlns:oc" : "http://owncloud.org/ns" , "xmlns:nc" : "http://nextcloud.org/ns" },
@@ -269,7 +356,14 @@ def listfav(self) -> list[FsNode]:
269356 check_error (webdav_response .status_code , request_info )
270357 return self ._lf_parse_webdav_records (webdav_response , self ._session .user , request_info , favorite = True )
271358
272- def setfav (self , path : str , value : Union [int , bool ]) -> None :
359+ def setfav (self , path : Union [str , FsNode ], value : Union [int , bool ]) -> None :
360+ """Sets or unsets favourite flag for specific file.
361+
362+ :param path: Path to the object to set the state.
363+ :param value: The value to set for the ``favourite`` state.
364+ """
365+
366+ path = path .path if isinstance (path , FsNode ) else path
273367 root = ElementTree .Element (
274368 "d:propertyupdate" ,
275369 attrib = {"xmlns:d" : "DAV:" , "xmlns:oc" : "http://owncloud.org/ns" },
@@ -326,8 +420,8 @@ def _parse_records(self, fs_records: list[dict], user: str, favorite: bool):
326420 return result
327421
328422 @staticmethod
329- def _parse_record (prop_stats : list [dict ], user : str , obg_rel_path : str , obj_name : str ) -> FsNode :
330- fs_node = FsNode (user = user , path = obg_rel_path , name = obj_name )
423+ def _parse_record (prop_stats : list [dict ], user : str , obj_rel_path : str , obj_name : str ) -> FsNode :
424+ fs_node = FsNode (user = user , path = obj_rel_path , name = obj_name )
331425 for prop_stat in prop_stats :
332426 if str (prop_stat .get ("d:status" , "" )).find ("200 OK" ) == - 1 :
333427 continue
0 commit comments