1515from nextcloud .base import WebDAVApiWrapper
1616from nextcloud .common .collections import PropertySet
1717from nextcloud .common .properties import Property as Prop , NAMESPACES_MAP
18- from nextcloud .common .value_parsing import timestamp_to_epoch_time
18+ from nextcloud .common .value_parsing import (
19+ timestamp_to_epoch_time ,
20+ datetime_to_timestamp
21+ )
22+
23+
24+ class NextCloudFileConflict (Exception ):
25+ """ Exception to raise when you try to create a File that alreay exists """
1926
2027
2128class File (PropertySet ):
29+ """
30+ Define properties on a WebDav file/folder
31+
32+ Additionnally, provide an objective CRUD API
33+ (that probably consume more energy than fetching specific attributes)
34+
35+ Example :
36+ >>> root = nxc.get_folder() # get root
37+ >>> def _list_rec(d, indent=""):
38+ >>> # list files recursively
39+ >>> print("%s%s%s" % (indent, d.basename(), '/' if d.isdir() else ''))
40+ >>> if d.isdir():
41+ >>> for i in d.list():
42+ >>> _list_rec(i, indent=indent+" ")
43+ >>>
44+ >>> _list_rec(root)
45+ """
2246 _attrs = [
2347 Prop ('d:getlastmodified' ),
2448 Prop ('d:getetag' ),
@@ -56,6 +80,97 @@ def isdir(self):
5680 """ say if the file is a directory /!\\ ressourcetype property shall be loaded """
5781 return self .resource_type == self .COLLECTION_RESOURCE_TYPE
5882
83+ def get_relative_path (self ):
84+ """ get path relative to user root """
85+ return self ._wrapper .get_relative_path (self .href )
86+
87+ def _get_remote_path (self , path = None ):
88+ _url = self .get_relative_path ()
89+ return '/' .join ([_url , path ]) if path else _url
90+
91+ def basename (self ):
92+ """ basename """
93+ _path = self ._get_remote_path ()
94+ return _path .split ('/' )[- 2 ] if _path .endswith ('/' ) else _path .split ('/' )[- 1 ]
95+
96+ def dirname (self ):
97+ """ dirname """
98+ _path = self ._get_remote_path ()
99+ return '/' .join (_path .split ('/' )[:- 2 ]) if _path .endswith ('/' ) else '/' .join (_path .split ('/' )[:- 1 ])
100+
101+ def __eq__ (self , b ):
102+ return self .href == b .href
103+
104+ # MINIMAL SET OF CRUD OPERATIONS
105+ def get_folder (self , path = None ):
106+ """
107+ Get folder (see WebDav wrapper)
108+ :param subpath: if empty list current dir
109+ :returns: a folder (File object)
110+
111+ Note : To check if sub folder exists, use get_file method
112+ """
113+ return self ._wrapper .get_folder (self ._get_remote_path (path ))
114+
115+ def get_folder (self , path = None ):
116+ """
117+ Get folder (see WebDav wrapper)
118+ :param subpath: if empty list current dir
119+ :returns: a file or folder (File object)
120+ """
121+ return self ._wrapper .get_file (self ._get_remote_path (path ))
122+
123+ def list (self , subpath = '' ):
124+ """
125+ List folder (see WebDav wrapper)
126+ :param subpath: if empty list current dir
127+ :returns: list of Files
128+ """
129+ resp = self ._wrapper .list_folders (
130+ self ._get_remote_path (subpath ),
131+ depth = 1 ,
132+ all_properties = True
133+ )
134+ if resp .is_ok and resp .data :
135+ _dirs = resp .data
136+ # remove current dir
137+ if _dirs [0 ] == self :
138+ _dirs = _dirs [1 :]
139+ return _dirs
140+ return []
141+
142+ def upload_file (self , local_filepath , name , timestamp = None ):
143+ """
144+ Upload file (see WebDav wrapper)
145+ :param name: name of the new file
146+ :returns: True if success
147+ """
148+ resp = self ._wrapper .upload_file (local_filepath ,
149+ self ._get_remote_path (name ),
150+ timestamp = timestamp )
151+ return resp .is_ok
152+
153+ def download (self , name = None , target_dir = None ):
154+ """
155+ file (see WebDav wrapper)
156+ :param name: name of the new file
157+ :returns: True if success
158+ """
159+ path = self ._get_remote_path (name )
160+ target_path , _file_info = self ._wrapper .download_file (path ,
161+ target_dir = target_dir )
162+ assert os .path .isfile (target_path ), "Download failed"
163+ return target_path
164+
165+ def delete (self , subpath = '' ):
166+ """
167+ Delete file or folder (see WebDav wrapper)
168+ :param subpath: if empty, delete current file
169+ :returns: True if success
170+ """
171+ resp = self ._wrapper .delete_path (self ._get_remote_path (subpath ))
172+ return resp .is_ok
173+
59174
60175class WebDAV (WebDAVApiWrapper ):
61176 """ WebDav API wrapper """
@@ -90,9 +205,10 @@ def list_folders(self, path=None, depth=1, all_properties=False,
90205 resp = self .requester .propfind (additional_url = self ._get_path (path ),
91206 headers = {'Depth' : str (depth )},
92207 data = data )
93- return File .from_response (resp , json_output = (self .json_output ))
208+ return File .from_response (resp , json_output = self .json_output ,
209+ wrapper = self )
94210
95- def download_file (self , path ):
211+ def download_file (self , path , target_dir = None ):
96212 """
97213 Download file by path (for current user)
98214 File will be saved to working directory
@@ -108,32 +224,35 @@ def download_file(self, path):
108224 path (str): file path
109225
110226 Returns:
111- None
227+ a tuple (target_path, File object)
112228 """
229+ if not target_dir :
230+ target_dir = './'
113231 filename = path .split ('/' )[(- 1 )] if '/' in path else path
114- file_data = self .list_folders (path = path , depth = 0 )
232+ file_data = self .get_file (path )
115233 if not file_data :
116234 raise ValueError ("Given path doesn't exist" )
117- file_resource_type = (file_data .data [0 ].get ('resource_type' )
118- if self .json_output
119- else file_data .data [0 ].resource_type )
235+ file_resource_type = file_data .resource_type
120236 if file_resource_type == File .COLLECTION_RESOURCE_TYPE :
121237 raise ValueError ("This is a collection, please specify file path" )
122- if filename in os .listdir ('./' ):
123- raise ValueError ( "File with such name already exists in this directory" )
238+ if filename in os .listdir (target_dir ):
239+ raise ValueError (
240+ "File with such name already exists in this directory" )
241+ filename = os .path .join (target_dir , filename )
124242 res = self .requester .download (self ._get_path (path ))
125243 with open (filename , 'wb' ) as f :
126244 f .write (res .data )
127245
128246 # get timestamp of downloaded file from file property on Nextcloud
129247 # If it succeeded, set the timestamp to saved local file
130248 # If the timestamp string is invalid or broken, the timestamp is downloaded time.
131- file_timestamp_str = (file_data .data [0 ].get ('last_modified' )
132- if self .json_output
133- else file_data .data [0 ].last_modified )
249+ file_timestamp_str = file_data .last_modified
134250 file_timestamp = timestamp_to_epoch_time (file_timestamp_str )
135251 if isinstance (file_timestamp , int ):
136- os .utime (filename , (datetime .now ().timestamp (), file_timestamp ))
252+ os .utime (filename , (
253+ datetime_to_timestamp (datetime .now ()),
254+ file_timestamp ))
255+ return (filename , file_data )
137256
138257 def upload_file (self , local_filepath , remote_filepath , timestamp = None ):
139258 """
@@ -233,7 +352,8 @@ def move_path(self, path, destination_path, overwrite=False):
233352 requester response
234353 """
235354 return self .requester .move (url = self ._get_path (path ),
236- destination = self ._get_path (destination_path ),
355+ destination = self ._get_path (
356+ destination_path ),
237357 overwrite = overwrite )
238358
239359 def copy_path (self , path , destination_path , overwrite = False ):
@@ -249,7 +369,8 @@ def copy_path(self, path, destination_path, overwrite=False):
249369 requester response
250370 """
251371 return self .requester .copy (url = self ._get_path (path ),
252- destination = self ._get_path (destination_path ),
372+ destination = self ._get_path (
373+ destination_path ),
253374 overwrite = overwrite )
254375
255376 def set_favorites (self , path ):
@@ -277,8 +398,10 @@ def list_favorites(self, path=''):
277398 """
278399 data = File .build_xml_propfind (
279400 instr = 'oc:filter-files' , filter_rules = {'oc' : {'favorite' : 1 }})
280- resp = self .requester .report (additional_url = self ._get_path (path ), data = data )
281- return File .from_response (resp , json_output = self .json_output )
401+ resp = self .requester .report (
402+ additional_url = self ._get_path (path ), data = data )
403+ return File .from_response (resp , json_output = self .json_output ,
404+ wrapper = self )
282405
283406 def get_file_property (self , path , field , tag = 'oc' ):
284407 """
@@ -310,3 +433,45 @@ def get_file_property(self, path, field, tag='oc'):
310433 break
311434
312435 return resp
436+
437+ def get_file (self , path ):
438+ """
439+ Return the File object associated to the path
440+
441+ :param path: path to the file
442+ :returns: File object or None
443+ """
444+ resp = self .client .with_attr (json_output = False ).list_folders (
445+ path , all_properties = True , depth = 0 )
446+ if resp .is_ok :
447+ if resp .data :
448+ return resp .data [0 ]
449+ return None
450+
451+ def get_folder (self , path = None ):
452+ """
453+ Return the File object associated to the path
454+ If the file (folder or 'collection') doesn't exists, create it.
455+
456+ :param path: path to the file/folder, if empty use root
457+ :returns: File object
458+ """
459+ fileobj = self .get_file (path )
460+ if fileobj :
461+ if not fileobj .isdir ():
462+ raise NextCloudFileConflict (fileobj .href )
463+ else :
464+ self .client .create_folder (path )
465+ fileobj = self .get_file (path )
466+
467+ return fileobj
468+
469+ def get_relative_path (self , href ):
470+ """
471+ Returns relative (to application / user) path
472+
473+ :param href(str): file href
474+ :returns (str): relative path
475+ """
476+ _app_root = '/' .join ([self .API_URL , self .client .user ])
477+ return href [len (_app_root ):]
0 commit comments