15
15
import xmltodict
16
16
from httpx import Response
17
17
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
20
20
from .._session import NcSessionBasic
21
- from . import FsNode
21
+ from . import FsNode , SystemTag
22
22
from .sharing import _FilesSharingAPI
23
23
24
24
PROPFIND_PROPERTIES = [
@@ -60,9 +60,8 @@ class PropFindType(enum.IntEnum):
60
60
61
61
DEFAULT = 0
62
62
TRASHBIN = 1
63
- FAVORITE = 2
64
- VERSIONS_FILEID = 3
65
- VERSIONS_FILE_ID = 4
63
+ VERSIONS_FILEID = 2
64
+ VERSIONS_FILE_ID = 3
66
65
67
66
68
67
class FilesAPI :
@@ -130,7 +129,7 @@ def find(self, req: list, path: Union[str, FsNode] = "") -> list[FsNode]:
130
129
headers = {"Content-Type" : "text/xml" }
131
130
webdav_response = self ._session .dav ("SEARCH" , "" , data = self ._element_tree_as_str (root ), headers = headers )
132
131
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 )
134
133
135
134
def download (self , path : Union [str , FsNode ]) -> bytes :
136
135
"""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
305
304
check_error (response .status_code , f"copy: user={ self ._session .user } , src={ path_src } , dest={ dest } , { overwrite } " )
306
305
return self .find (req = ["eq" , "fileid" , response .headers ["OC-FileId" ]])[0 ]
307
306
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." )
310
318
root = ElementTree .Element (
311
319
"oc:filter-files" ,
312
320
attrib = {"xmlns:d" : "DAV:" , "xmlns:oc" : "http://owncloud.org/ns" , "xmlns:nc" : "http://nextcloud.org/ns" },
313
321
)
322
+ prop = ElementTree .SubElement (root , "d:prop" )
323
+ for i in PROPFIND_PROPERTIES :
324
+ ElementTree .SubElement (prop , i )
314
325
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 )
316
332
webdav_response = self ._session .dav (
317
333
"REPORT" , self ._dav_get_obj_path (self ._session .user ), data = self ._element_tree_as_str (root )
318
334
)
319
- request_info = f"listfav : { self ._session .user } "
335
+ request_info = f"list_files_by_criteria : { self ._session .user } "
320
336
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 )
322
338
323
339
def setfav (self , path : Union [str , FsNode ], value : Union [int , bool ]) -> None :
324
340
"""Sets or unsets favourite flag for specific file.
@@ -408,6 +424,108 @@ def restore_version(self, file_object: FsNode) -> None:
408
424
)
409
425
check_error (response .status_code , f"restore_version: user={ self ._session .user } , src={ file_object .user_path } " )
410
426
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
+
411
529
def _listdir (
412
530
self ,
413
531
user : str ,
@@ -437,7 +555,7 @@ def _listdir(
437
555
headers = {"Depth" : "infinity" if depth == - 1 else str (depth )},
438
556
)
439
557
440
- result = self ._lf_parse_webdav_records (
558
+ result = self ._lf_parse_webdav_response (
441
559
webdav_response ,
442
560
f"list: { user } , { path } , { properties } " ,
443
561
prop_type ,
@@ -467,12 +585,7 @@ def _parse_records(self, fs_records: list[dict], response_type: PropFindType) ->
467
585
fs_node .file_id = str (fs_node .info .fileid )
468
586
else :
469
587
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 :
476
589
result .append (fs_node )
477
590
return result
478
591
@@ -509,9 +622,13 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
509
622
# xz = prop.get("oc:dDC", "")
510
623
return FsNode (full_path , ** fs_node_args )
511
624
512
- def _lf_parse_webdav_records (
625
+ def _lf_parse_webdav_response (
513
626
self , webdav_res : Response , info : str , response_type : PropFindType = PropFindType .DEFAULT
514
627
) -> 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 ]:
515
632
check_error (webdav_res .status_code , info = info )
516
633
if webdav_res .status_code != 207 : # multistatus
517
634
raise NextcloudException (webdav_res .status_code , "Response is not a multistatus." , info = info )
@@ -520,7 +637,7 @@ def _lf_parse_webdav_records(
520
637
err = response_data ["d:error" ]
521
638
raise NextcloudException (reason = f'{ err ["s:exception" ]} : { err ["s:message" ]} ' .replace ("\n " , "" ), info = info )
522
639
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
524
641
525
642
@staticmethod
526
643
def _dav_get_obj_path (user : str , path : str = "" , root_path = "/files" ) -> str :
0 commit comments