11"""Nextcloud API for working with the file system."""
22
33import builtins
4+ import enum
45import os
56from io import BytesIO
67from json import dumps , loads
1516from httpx import Response
1617
1718from .._exceptions import NextcloudException , check_error
19+ from .._misc import require_capabilities
1820from .._session import NcSessionBasic
1921from . import FsNode
2022from .sharing import _FilesSharingAPI
5355}
5456
5557
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+
5668class FilesAPI :
5769 """Class that encapsulates the file system and file sharing functionality."""
5870
@@ -305,7 +317,7 @@ def listfav(self) -> list[FsNode]:
305317 )
306318 request_info = f"listfav: { self ._session .user } "
307319 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 )
309321
310322 def setfav (self , path : Union [str , FsNode ], value : Union [int , bool ]) -> None :
311323 """Sets or unsets favourite flag for specific file.
@@ -330,7 +342,9 @@ def trashbin_list(self) -> list[FsNode]:
330342 """Returns a list of all entries in the TrashBin."""
331343 properties = PROPFIND_PROPERTIES
332344 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+ )
334348
335349 def trashbin_restore (self , path : Union [str , FsNode ]) -> None :
336350 """Restore a file/directory from the TrashBin.
@@ -366,8 +380,41 @@ def trashbin_cleanup(self) -> None:
366380 response = self ._session .dav (method = "DELETE" , path = f"/trashbin/{ self ._session .user } /trash" )
367381 check_error (response .status_code , f"trashbin_cleanup: user={ self ._session .user } " )
368382
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+
369410 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 ,
371418 ) -> list [FsNode ]:
372419 root = ElementTree .Element (
373420 "d:propfind" ,
@@ -376,7 +423,9 @@ def _listdir(
376423 prop = ElementTree .SubElement (root , "d:prop" )
377424 for i in properties :
378425 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 :
380429 dav_path = self ._dav_get_obj_path (f"trashbin/{ user } /trash" , path , root_path = "" )
381430 else :
382431 dav_path = self ._dav_get_obj_path (user , path )
@@ -386,23 +435,38 @@ def _listdir(
386435 self ._element_tree_as_str (root ),
387436 headers = {"Depth" : "infinity" if depth == - 1 else str (depth )},
388437 )
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+ )
391444 if exclude_self :
392445 for index , v in enumerate (result ):
393446 if v .user_path .rstrip ("/" ) == path .rstrip ("/" ):
394447 del result [index ]
395448 break
396449 return result
397450
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 ] :
399452 result : list [FsNode ] = []
400453 for record in fs_records :
401454 obj_full_path = unquote (record .get ("d:href" , "" ))
402455 obj_full_path = obj_full_path .replace (self ._session .cfg .dav_url_suffix , "" ).lstrip ("/" )
403456 propstat = record ["d:propstat" ]
404457 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 :
406470 _fs_node = self .by_path (fs_node .user_path )
407471 if _fs_node :
408472 _fs_node .info .favorite = True
@@ -444,7 +508,9 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
444508 # xz = prop.get("oc:dDC", "")
445509 return FsNode (full_path , ** fs_node_args )
446510
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 ]:
448514 check_error (webdav_res .status_code , info = info )
449515 if webdav_res .status_code != 207 : # multistatus
450516 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
453519 err = response_data ["d:error" ]
454520 raise NextcloudException (reason = f'{ err ["s:exception" ]} : { err ["s:message" ]} ' .replace ("\n " , "" ), info = info )
455521 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 )
457523
458524 @staticmethod
459525 def _dav_get_obj_path (user : str , path : str = "" , root_path = "/files" ) -> str :
0 commit comments