1515import xmltodict
1616from httpx import Response
1717
18- from .._exceptions import NextcloudException , check_error
19- from .._misc import require_capabilities
18+ from .._exceptions import NextcloudException , NextcloudExceptionNotFound , check_error
19+ from .._misc import clear_from_params_empty , require_capabilities
2020from .._session import NcSessionBasic
21- from . import FsNode
21+ from . import FsNode , SystemTag
2222from .sharing import _FilesSharingAPI
2323
2424PROPFIND_PROPERTIES = [
@@ -60,9 +60,8 @@ class PropFindType(enum.IntEnum):
6060
6161 DEFAULT = 0
6262 TRASHBIN = 1
63- FAVORITE = 2
64- VERSIONS_FILEID = 3
65- VERSIONS_FILE_ID = 4
63+ VERSIONS_FILEID = 2
64+ VERSIONS_FILE_ID = 3
6665
6766
6867class FilesAPI :
@@ -130,7 +129,7 @@ def find(self, req: list, path: Union[str, FsNode] = "") -> list[FsNode]:
130129 headers = {"Content-Type" : "text/xml" }
131130 webdav_response = self ._session .dav ("SEARCH" , "" , data = self ._element_tree_as_str (root ), headers = headers )
132131 request_info = f"find: { self ._session .user } , { req } , { path } "
133- return self ._lf_parse_webdav_records (webdav_response , request_info )
132+ return self ._lf_parse_webdav_response (webdav_response , request_info )
134133
135134 def download (self , path : Union [str , FsNode ]) -> bytes :
136135 """Downloads and returns the content of a file.
@@ -305,20 +304,37 @@ def copy(self, path_src: Union[str, FsNode], path_dest: Union[str, FsNode], over
305304 check_error (response .status_code , f"copy: user={ self ._session .user } , src={ path_src } , dest={ dest } , { overwrite } " )
306305 return self .find (req = ["eq" , "fileid" , response .headers ["OC-FileId" ]])[0 ]
307306
308- def listfav (self ) -> list [FsNode ]:
309- """Returns a list of the current user's favorite files."""
307+ def list_by_criteria (
308+ self , properties : Optional [list [str ]] = None , tags : Optional [list [Union [int , SystemTag ]]] = None
309+ ) -> list [FsNode ]:
310+ """Returns a list of all files/directories for the current user filtered by the specified values.
311+
312+ :param properties: List of ``properties`` that should have been set for the file.
313+ Supported values: **favorite**
314+ :param tags: List of ``tags ids`` or ``SystemTag`` that should have been set for the file.
315+ """
316+ if not properties and not tags :
317+ raise ValueError ("Either specify 'properties' or 'tags' to filter results." )
310318 root = ElementTree .Element (
311319 "oc:filter-files" ,
312320 attrib = {"xmlns:d" : "DAV:" , "xmlns:oc" : "http://owncloud.org/ns" , "xmlns:nc" : "http://nextcloud.org/ns" },
313321 )
322+ prop = ElementTree .SubElement (root , "d:prop" )
323+ for i in PROPFIND_PROPERTIES :
324+ ElementTree .SubElement (prop , i )
314325 xml_filter_rules = ElementTree .SubElement (root , "oc:filter-rules" )
315- ElementTree .SubElement (xml_filter_rules , "oc:favorite" ).text = "1"
326+ if properties and "favorite" in properties :
327+ ElementTree .SubElement (xml_filter_rules , "oc:favorite" ).text = "1"
328+ if tags :
329+ for v in tags :
330+ tag_id = v .tag_id if isinstance (v , SystemTag ) else v
331+ ElementTree .SubElement (xml_filter_rules , "oc:systemtag" ).text = str (tag_id )
316332 webdav_response = self ._session .dav (
317333 "REPORT" , self ._dav_get_obj_path (self ._session .user ), data = self ._element_tree_as_str (root )
318334 )
319- request_info = f"listfav : { self ._session .user } "
335+ request_info = f"list_files_by_criteria : { self ._session .user } "
320336 check_error (webdav_response .status_code , request_info )
321- return self ._lf_parse_webdav_records (webdav_response , request_info , PropFindType . FAVORITE )
337+ return self ._lf_parse_webdav_response (webdav_response , request_info )
322338
323339 def setfav (self , path : Union [str , FsNode ], value : Union [int , bool ]) -> None :
324340 """Sets or unsets favourite flag for specific file.
@@ -408,6 +424,108 @@ def restore_version(self, file_object: FsNode) -> None:
408424 )
409425 check_error (response .status_code , f"restore_version: user={ self ._session .user } , src={ file_object .user_path } " )
410426
427+ def list_tags (self ) -> list [SystemTag ]:
428+ """Returns list of the avalaible Tags."""
429+ root = ElementTree .Element (
430+ "d:propfind" ,
431+ attrib = {"xmlns:d" : "DAV:" , "xmlns:oc" : "http://owncloud.org/ns" },
432+ )
433+ properties = ["oc:id" , "oc:display-name" , "oc:user-visible" , "oc:user-assignable" ]
434+ prop_element = ElementTree .SubElement (root , "d:prop" )
435+ for i in properties :
436+ ElementTree .SubElement (prop_element , i )
437+ response = self ._session .dav ("PROPFIND" , "/systemtags" , self ._element_tree_as_str (root ))
438+ result = []
439+ records = self ._webdav_response_to_records (response , "list_tags" )
440+ for record in records :
441+ prop_stat = record ["d:propstat" ]
442+ if str (prop_stat .get ("d:status" , "" )).find ("200 OK" ) == - 1 :
443+ continue
444+ result .append (SystemTag (prop_stat ["d:prop" ]))
445+ return result
446+
447+ def create_tag (self , name : str , user_visible : bool = True , user_assignable : bool = True ) -> None :
448+ """Creates a new Tag.
449+
450+ :param name: Name of the tag.
451+ :param user_visible: Should be Tag visible in the UI.
452+ :param user_assignable: Can Tag be assigned from the UI.
453+ """
454+ response = self ._session .dav (
455+ "POST" ,
456+ path = "/systemtags" ,
457+ json = {
458+ "name" : name ,
459+ "userVisible" : user_visible ,
460+ "userAssignable" : user_assignable ,
461+ },
462+ )
463+ check_error (response .status_code , info = f"create_tag({ name } )" )
464+
465+ def update_tag (
466+ self ,
467+ tag_id : Union [int , SystemTag ],
468+ name : Optional [str ] = None ,
469+ user_visible : Optional [bool ] = None ,
470+ user_assignable : Optional [bool ] = None ,
471+ ) -> None :
472+ """Updates the Tag information."""
473+ tag_id = tag_id .tag_id if isinstance (tag_id , SystemTag ) else tag_id
474+ root = ElementTree .Element (
475+ "d:propertyupdate" ,
476+ attrib = {
477+ "xmlns:d" : "DAV:" ,
478+ "xmlns:oc" : "http://owncloud.org/ns" ,
479+ },
480+ )
481+ properties = {
482+ "oc:display-name" : name ,
483+ "oc:user-visible" : "true" if user_visible is True else "false" if user_visible is False else None ,
484+ "oc:user-assignable" : "true" if user_assignable is True else "false" if user_assignable is False else None ,
485+ }
486+ clear_from_params_empty (list (properties .keys ()), properties )
487+ if not properties :
488+ raise ValueError ("No property specified to change." )
489+ xml_set = ElementTree .SubElement (root , "d:set" )
490+ prop_element = ElementTree .SubElement (xml_set , "d:prop" )
491+ for k , v in properties .items ():
492+ ElementTree .SubElement (prop_element , k ).text = v
493+ response = self ._session .dav ("PROPPATCH" , f"/systemtags/{ tag_id } " , self ._element_tree_as_str (root ))
494+ check_error (response .status_code , info = f"update_tag({ tag_id } )" )
495+
496+ def delete_tag (self , tag_id : Union [int , SystemTag ]) -> None :
497+ """Deletes the tag."""
498+ tag_id = tag_id .tag_id if isinstance (tag_id , SystemTag ) else tag_id
499+ response = self ._session .dav ("DELETE" , f"/systemtags/{ tag_id } " )
500+ check_error (response .status_code , info = f"delete_tag({ tag_id } )" )
501+
502+ def tag_by_name (self , tag_name : str ) -> SystemTag :
503+ """Returns Tag info by its name if found or ``None`` otherwise."""
504+ r = [i for i in self .list_tags () if i .display_name == tag_name ]
505+ if not r :
506+ raise NextcloudExceptionNotFound (f"Tag with name='{ tag_name } ' not found." )
507+ return r [0 ]
508+
509+ def assign_tag (self , file_id : Union [FsNode , int ], tag_id : Union [SystemTag , int ]) -> None :
510+ """Assigns Tag to a file/directory."""
511+ self ._file_change_tag_state (file_id , tag_id , True )
512+
513+ def unassign_tag (self , file_id : Union [FsNode , int ], tag_id : Union [SystemTag , int ]) -> None :
514+ """Removes Tag from a file/directory."""
515+ self ._file_change_tag_state (file_id , tag_id , False )
516+
517+ def _file_change_tag_state (
518+ self , file_id : Union [FsNode , int ], tag_id : Union [SystemTag , int ], tag_state : bool
519+ ) -> None :
520+ request = "PUT" if tag_state else "DELETE"
521+ fs_object = file_id .info .fileid if isinstance (file_id , FsNode ) else file_id
522+ tag = tag_id .tag_id if isinstance (tag_id , SystemTag ) else tag_id
523+ response = self ._session .dav (request , f"/systemtags-relations/files/{ fs_object } /{ tag } " )
524+ check_error (
525+ response .status_code ,
526+ info = f"({ 'Adding' if tag_state else 'Removing' } `{ tag } ` { 'to' if tag_state else 'from' } { fs_object } )" ,
527+ )
528+
411529 def _listdir (
412530 self ,
413531 user : str ,
@@ -437,7 +555,7 @@ def _listdir(
437555 headers = {"Depth" : "infinity" if depth == - 1 else str (depth )},
438556 )
439557
440- result = self ._lf_parse_webdav_records (
558+ result = self ._lf_parse_webdav_response (
441559 webdav_response ,
442560 f"list: { user } , { path } , { properties } " ,
443561 prop_type ,
@@ -467,12 +585,7 @@ def _parse_records(self, fs_records: list[dict], response_type: PropFindType) ->
467585 fs_node .file_id = str (fs_node .info .fileid )
468586 else :
469587 fs_node .file_id = fs_node .full_path .rsplit ("/" , 2 )[- 2 ]
470- if response_type == PropFindType .FAVORITE and not fs_node .file_id :
471- _fs_node = self .by_path (fs_node .user_path )
472- if _fs_node :
473- _fs_node .info .favorite = True
474- result .append (_fs_node )
475- elif fs_node .file_id :
588+ if fs_node .file_id :
476589 result .append (fs_node )
477590 return result
478591
@@ -509,9 +622,13 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
509622 # xz = prop.get("oc:dDC", "")
510623 return FsNode (full_path , ** fs_node_args )
511624
512- def _lf_parse_webdav_records (
625+ def _lf_parse_webdav_response (
513626 self , webdav_res : Response , info : str , response_type : PropFindType = PropFindType .DEFAULT
514627 ) -> list [FsNode ]:
628+ return self ._parse_records (self ._webdav_response_to_records (webdav_res , info ), response_type )
629+
630+ @staticmethod
631+ def _webdav_response_to_records (webdav_res : Response , info : str ) -> list [dict ]:
515632 check_error (webdav_res .status_code , info = info )
516633 if webdav_res .status_code != 207 : # multistatus
517634 raise NextcloudException (webdav_res .status_code , "Response is not a multistatus." , info = info )
@@ -520,7 +637,7 @@ def _lf_parse_webdav_records(
520637 err = response_data ["d:error" ]
521638 raise NextcloudException (reason = f'{ err ["s:exception" ]} : { err ["s:message" ]} ' .replace ("\n " , "" ), info = info )
522639 response = response_data ["d:multistatus" ].get ("d:response" , [])
523- return self . _parse_records ( [response ] if isinstance (response , dict ) else response , response_type )
640+ return [response ] if isinstance (response , dict ) else response
524641
525642 @staticmethod
526643 def _dav_get_obj_path (user : str , path : str = "" , root_path = "/files" ) -> str :
0 commit comments