1
1
"""Nextcloud API for working with the file system."""
2
2
3
3
import builtins
4
+ import enum
4
5
import os
5
6
from io import BytesIO
6
7
from json import dumps , loads
15
16
from httpx import Response
16
17
17
18
from .._exceptions import NextcloudException , check_error
19
+ from .._misc import require_capabilities
18
20
from .._session import NcSessionBasic
19
21
from . import FsNode
20
22
from .sharing import _FilesSharingAPI
53
55
}
54
56
55
57
58
+ class PropFindType (enum .IntEnum ):
59
+ """Internal enum types for ``_listdir`` and ``_lf_parse_webdav_records`` methods."""
60
+
61
+ DEFAULT = 0
62
+ TRASHBIN = 1
63
+ FAVORITE = 2
64
+ VERSIONS_FILEID = 3
65
+ VERSIONS_FILE_ID = 4
66
+
67
+
56
68
class FilesAPI :
57
69
"""Class that encapsulates the file system and file sharing functionality."""
58
70
@@ -305,7 +317,7 @@ def listfav(self) -> list[FsNode]:
305
317
)
306
318
request_info = f"listfav: { self ._session .user } "
307
319
check_error (webdav_response .status_code , request_info )
308
- return self ._lf_parse_webdav_records (webdav_response , request_info , favorite = True )
320
+ return self ._lf_parse_webdav_records (webdav_response , request_info , PropFindType . FAVORITE )
309
321
310
322
def setfav (self , path : Union [str , FsNode ], value : Union [int , bool ]) -> None :
311
323
"""Sets or unsets favourite flag for specific file.
@@ -330,7 +342,9 @@ def trashbin_list(self) -> list[FsNode]:
330
342
"""Returns a list of all entries in the TrashBin."""
331
343
properties = PROPFIND_PROPERTIES
332
344
properties += ["nc:trashbin-filename" , "nc:trashbin-original-location" , "nc:trashbin-deletion-time" ]
333
- return self ._listdir (self ._session .user , "" , properties = properties , depth = 1 , exclude_self = False , trashbin = True )
345
+ return self ._listdir (
346
+ self ._session .user , "" , properties = properties , depth = 1 , exclude_self = False , prop_type = PropFindType .TRASHBIN
347
+ )
334
348
335
349
def trashbin_restore (self , path : Union [str , FsNode ]) -> None :
336
350
"""Restore a file/directory from the TrashBin.
@@ -366,8 +380,41 @@ def trashbin_cleanup(self) -> None:
366
380
response = self ._session .dav (method = "DELETE" , path = f"/trashbin/{ self ._session .user } /trash" )
367
381
check_error (response .status_code , f"trashbin_cleanup: user={ self ._session .user } " )
368
382
383
+ def get_versions (self , file_object : FsNode ) -> list [FsNode ]:
384
+ """Returns a list of all file versions if any."""
385
+ require_capabilities ("files.versioning" , self ._session .capabilities )
386
+ return self ._listdir (
387
+ self ._session .user ,
388
+ str (file_object .info .fileid ) if file_object .info .fileid else file_object .file_id ,
389
+ properties = PROPFIND_PROPERTIES ,
390
+ depth = 1 ,
391
+ exclude_self = False ,
392
+ prop_type = PropFindType .VERSIONS_FILEID if file_object .info .fileid else PropFindType .VERSIONS_FILE_ID ,
393
+ )
394
+
395
+ def restore_version (self , file_object : FsNode ) -> None :
396
+ """Restore a file with specified version.
397
+
398
+ :param file_object: The **FsNode** class from :py:meth:`~nc_py_api.files.files.FilesAPI.get_versions`.
399
+ """
400
+ require_capabilities ("files.versioning" , self ._session .capabilities )
401
+ dest = self ._session .cfg .dav_endpoint + f"/versions/{ self ._session .user } /restore/{ file_object .name } "
402
+ headers = {"Destination" : dest }
403
+ response = self ._session .dav (
404
+ "MOVE" ,
405
+ path = f"/versions/{ self ._session .user } /{ file_object .user_path } " ,
406
+ headers = headers ,
407
+ )
408
+ check_error (response .status_code , f"restore_version: user={ self ._session .user } , src={ file_object .user_path } " )
409
+
369
410
def _listdir (
370
- self , user : str , path : str , properties : list [str ], depth : int , exclude_self : bool , trashbin : bool = False
411
+ self ,
412
+ user : str ,
413
+ path : str ,
414
+ properties : list [str ],
415
+ depth : int ,
416
+ exclude_self : bool ,
417
+ prop_type : PropFindType = PropFindType .DEFAULT ,
371
418
) -> list [FsNode ]:
372
419
root = ElementTree .Element (
373
420
"d:propfind" ,
@@ -376,7 +423,9 @@ def _listdir(
376
423
prop = ElementTree .SubElement (root , "d:prop" )
377
424
for i in properties :
378
425
ElementTree .SubElement (prop , i )
379
- if trashbin :
426
+ if prop_type in (PropFindType .VERSIONS_FILEID , PropFindType .VERSIONS_FILE_ID ):
427
+ dav_path = self ._dav_get_obj_path (f"versions/{ user } /versions" , path , root_path = "" )
428
+ elif prop_type == PropFindType .TRASHBIN :
380
429
dav_path = self ._dav_get_obj_path (f"trashbin/{ user } /trash" , path , root_path = "" )
381
430
else :
382
431
dav_path = self ._dav_get_obj_path (user , path )
@@ -386,23 +435,38 @@ def _listdir(
386
435
self ._element_tree_as_str (root ),
387
436
headers = {"Depth" : "infinity" if depth == - 1 else str (depth )},
388
437
)
389
- request_info = f"list: { user } , { path } , { properties } "
390
- result = self ._lf_parse_webdav_records (webdav_response , request_info )
438
+
439
+ result = self ._lf_parse_webdav_records (
440
+ webdav_response ,
441
+ f"list: { user } , { path } , { properties } " ,
442
+ prop_type ,
443
+ )
391
444
if exclude_self :
392
445
for index , v in enumerate (result ):
393
446
if v .user_path .rstrip ("/" ) == path .rstrip ("/" ):
394
447
del result [index ]
395
448
break
396
449
return result
397
450
398
- def _parse_records (self , fs_records : list [dict ], favorite : bool ) :
451
+ def _parse_records (self , fs_records : list [dict ], response_type : PropFindType ) -> list [ FsNode ] :
399
452
result : list [FsNode ] = []
400
453
for record in fs_records :
401
454
obj_full_path = unquote (record .get ("d:href" , "" ))
402
455
obj_full_path = obj_full_path .replace (self ._session .cfg .dav_url_suffix , "" ).lstrip ("/" )
403
456
propstat = record ["d:propstat" ]
404
457
fs_node = self ._parse_record (obj_full_path , propstat if isinstance (propstat , list ) else [propstat ])
405
- if favorite and not fs_node .file_id :
458
+ if fs_node .etag and response_type in (
459
+ PropFindType .VERSIONS_FILE_ID ,
460
+ PropFindType .VERSIONS_FILEID ,
461
+ ):
462
+ fs_node .full_path = fs_node .full_path .rstrip ("/" )
463
+ fs_node .info .is_version = True
464
+ if response_type == PropFindType .VERSIONS_FILEID :
465
+ fs_node .info .fileid = int (fs_node .full_path .rsplit ("/" , 2 )[- 2 ])
466
+ fs_node .file_id = str (fs_node .info .fileid )
467
+ else :
468
+ fs_node .file_id = fs_node .full_path .rsplit ("/" , 2 )[- 2 ]
469
+ if response_type == PropFindType .FAVORITE and not fs_node .file_id :
406
470
_fs_node = self .by_path (fs_node .user_path )
407
471
if _fs_node :
408
472
_fs_node .info .favorite = True
@@ -444,7 +508,9 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
444
508
# xz = prop.get("oc:dDC", "")
445
509
return FsNode (full_path , ** fs_node_args )
446
510
447
- def _lf_parse_webdav_records (self , webdav_res : Response , info : str , favorite = False ) -> list [FsNode ]:
511
+ def _lf_parse_webdav_records (
512
+ self , webdav_res : Response , info : str , response_type : PropFindType = PropFindType .DEFAULT
513
+ ) -> list [FsNode ]:
448
514
check_error (webdav_res .status_code , info = info )
449
515
if webdav_res .status_code != 207 : # multistatus
450
516
raise NextcloudException (webdav_res .status_code , "Response is not a multistatus." , info = info )
@@ -453,7 +519,7 @@ def _lf_parse_webdav_records(self, webdav_res: Response, info: str, favorite=Fal
453
519
err = response_data ["d:error" ]
454
520
raise NextcloudException (reason = f'{ err ["s:exception" ]} : { err ["s:message" ]} ' .replace ("\n " , "" ), info = info )
455
521
response = response_data ["d:multistatus" ].get ("d:response" , [])
456
- return self ._parse_records ([response ] if isinstance (response , dict ) else response , favorite )
522
+ return self ._parse_records ([response ] if isinstance (response , dict ) else response , response_type )
457
523
458
524
@staticmethod
459
525
def _dav_get_obj_path (user : str , path : str = "" , root_path = "/files" ) -> str :
0 commit comments