nc-py-api 0.10.0__py3-none-any.whl → 0.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nc_py_api/files/files.py CHANGED
@@ -9,8 +9,8 @@ from httpx import Headers
9
9
 
10
10
  from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
11
11
  from .._misc import random_string, require_capabilities
12
- from .._session import AsyncNcSessionBasic, NcSessionBasic
13
- from . import FsNode, SystemTag
12
+ from .._session import NcSessionBasic
13
+ from . import FsNode, LockType, SystemTag
14
14
  from ._files import (
15
15
  PROPFIND_PROPERTIES,
16
16
  PropFindType,
@@ -21,13 +21,15 @@ from ._files import (
21
21
  build_listdir_req,
22
22
  build_listdir_response,
23
23
  build_setfav_req,
24
+ build_tags_ids_for_object,
24
25
  build_update_tag_req,
25
26
  dav_get_obj_path,
26
27
  element_tree_as_str,
27
28
  etag_fileid_from_response,
29
+ get_propfind_properties,
28
30
  lf_parse_webdav_response,
29
31
  )
30
- from .sharing import _AsyncFilesSharingAPI, _FilesSharingAPI
32
+ from .sharing import _FilesSharingAPI
31
33
 
32
34
 
33
35
  class FilesAPI:
@@ -50,7 +52,7 @@ class FilesAPI:
50
52
  """
51
53
  if exclude_self and not depth:
52
54
  raise ValueError("Wrong input parameters, query will return nothing.")
53
- properties = PROPFIND_PROPERTIES
55
+ properties = get_propfind_properties(self._session.capabilities)
54
56
  path = path.user_path if isinstance(path, FsNode) else path
55
57
  return self._listdir(self._session.user, path, properties=properties, depth=depth, exclude_self=exclude_self)
56
58
 
@@ -76,7 +78,7 @@ class FilesAPI:
76
78
  :param path: path where to search from. Default = **""**.
77
79
  """
78
80
  # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
79
- root = build_find_request(req, path, self._session.user)
81
+ root = build_find_request(req, path, self._session.user, self._session.capabilities)
80
82
  webdav_response = self._session.adapter_dav.request(
81
83
  "SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
82
84
  )
@@ -113,9 +115,14 @@ class FilesAPI:
113
115
  path = path.user_path if isinstance(path, FsNode) else path
114
116
  result_path = local_path if local_path else os.path.basename(path)
115
117
  with open(result_path, "wb") as fp:
116
- self._session.download2fp(
117
- "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
118
- )
118
+ if self._session.nc_version["major"] >= 31:
119
+ full_path = dav_get_obj_path(self._session.user, path)
120
+ accept_header = f"application/{kwargs.get('format', 'zip')}"
121
+ self._session.download2fp(quote(full_path), fp, dav=True, headers={"Accept": accept_header})
122
+ else:
123
+ self._session.download2fp(
124
+ "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
125
+ )
119
126
  return Path(result_path)
120
127
 
121
128
  def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
@@ -172,7 +179,7 @@ class FilesAPI:
172
179
  path = path.lstrip("/")
173
180
  result = None
174
181
  for i in Path(path).parts:
175
- _path = os.path.join(_path, i)
182
+ _path = f"{_path}/{i}"
176
183
  if not exist_ok:
177
184
  result = self.mkdir(_path)
178
185
  else:
@@ -248,7 +255,7 @@ class FilesAPI:
248
255
  Supported values: **favorite**
249
256
  :param tags: List of ``tags ids`` or ``SystemTag`` that should have been set for the file.
250
257
  """
251
- root = build_list_by_criteria_req(properties, tags)
258
+ root = build_list_by_criteria_req(properties, tags, self._session.capabilities)
252
259
  webdav_response = self._session.adapter_dav.request(
253
260
  "REPORT", dav_get_obj_path(self._session.user), content=element_tree_as_str(root)
254
261
  )
@@ -332,7 +339,7 @@ class FilesAPI:
332
339
  headers = Headers({"Destination": dest}, encoding="utf-8")
333
340
  response = self._session.adapter_dav.request(
334
341
  "MOVE",
335
- f"/versions/{self._session.user}/{file_object.user_path}",
342
+ quote(f"/versions/{self._session.user}/{file_object.user_path}"),
336
343
  headers=headers,
337
344
  )
338
345
  check_error(response, f"restore_version: user={self._session.user}, src={file_object.user_path}")
@@ -343,6 +350,17 @@ class FilesAPI:
343
350
  response = self._session.adapter_dav.request("PROPFIND", "/systemtags", content=element_tree_as_str(root))
344
351
  return build_list_tags_response(response)
345
352
 
353
+ def get_tags(self, file_id: FsNode | int) -> list[SystemTag]:
354
+ """Returns list of Tags assigned to the File or Directory."""
355
+ fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
356
+ url_to_fetch = f"/systemtags-relations/files/{fs_object}/"
357
+ response = self._session.adapter_dav.request("PROPFIND", url_to_fetch)
358
+ object_tags_ids = build_tags_ids_for_object(self._session.cfg.dav_url_suffix + url_to_fetch, response)
359
+ if not object_tags_ids:
360
+ return []
361
+ all_tags = self.list_tags()
362
+ return [tag for tag in all_tags if tag.tag_id in object_tags_ids]
363
+
346
364
  def create_tag(self, name: str, user_visible: bool = True, user_assignable: bool = True) -> None:
347
365
  """Creates a new Tag.
348
366
 
@@ -396,6 +414,34 @@ class FilesAPI:
396
414
  """Removes Tag from a file/directory."""
397
415
  self._file_change_tag_state(file_id, tag_id, False)
398
416
 
417
+ def lock(self, path: FsNode | str, lock_type: LockType = LockType.MANUAL_LOCK) -> None:
418
+ """Locks the file.
419
+
420
+ .. note:: Exception codes: 423 - existing lock present.
421
+ """
422
+ require_capabilities("files.locking", self._session.capabilities)
423
+ full_path = dav_get_obj_path(self._session.user, path.user_path if isinstance(path, FsNode) else path)
424
+ response = self._session.adapter_dav.request(
425
+ "LOCK",
426
+ quote(full_path),
427
+ headers={"X-User-Lock": "1", "X-User-Lock-Type": str(lock_type.value)},
428
+ )
429
+ check_error(response, f"lock: user={self._session.user}, path={full_path}")
430
+
431
+ def unlock(self, path: FsNode | str) -> None:
432
+ """Unlocks the file.
433
+
434
+ .. note:: Exception codes: 412 - the file is not locked, 423 - the lock is owned by another user.
435
+ """
436
+ require_capabilities("files.locking", self._session.capabilities)
437
+ full_path = dav_get_obj_path(self._session.user, path.user_path if isinstance(path, FsNode) else path)
438
+ response = self._session.adapter_dav.request(
439
+ "UNLOCK",
440
+ quote(full_path),
441
+ headers={"X-User-Lock": "1"},
442
+ )
443
+ check_error(response, f"unlock: user={self._session.user}, path={full_path}")
444
+
399
445
  def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
400
446
  fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
401
447
  tag = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
@@ -439,7 +485,8 @@ class FilesAPI:
439
485
  response = self._session.adapter_dav.request("MKCOL", _dav_path)
440
486
  check_error(response)
441
487
  try:
442
- start_bytes = end_bytes = chunk_number = 0
488
+ start_bytes = end_bytes = 0
489
+ chunk_number = 1
443
490
  while True:
444
491
  piece = fp.read(chunk_size)
445
492
  if not piece:
@@ -471,455 +518,3 @@ class FilesAPI:
471
518
  return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
472
519
  finally:
473
520
  self._session.adapter_dav.delete(_dav_path)
474
-
475
-
476
- class AsyncFilesAPI:
477
- """Class that encapsulates async file system and file sharing API."""
478
-
479
- sharing: _AsyncFilesSharingAPI
480
- """API for managing Files Shares"""
481
-
482
- def __init__(self, session: AsyncNcSessionBasic):
483
- self._session = session
484
- self.sharing = _AsyncFilesSharingAPI(session)
485
-
486
- async def listdir(self, path: str | FsNode = "", depth: int = 1, exclude_self=True) -> list[FsNode]:
487
- """Returns a list of all entries in the specified directory.
488
-
489
- :param path: path to the directory to get the list.
490
- :param depth: how many directory levels should be included in output. Default = **1** (only specified directory)
491
- :param exclude_self: boolean value indicating whether the `path` itself should be excluded from the list or not.
492
- Default = **True**.
493
- """
494
- if exclude_self and not depth:
495
- raise ValueError("Wrong input parameters, query will return nothing.")
496
- properties = PROPFIND_PROPERTIES
497
- path = path.user_path if isinstance(path, FsNode) else path
498
- return await self._listdir(
499
- await self._session.user, path, properties=properties, depth=depth, exclude_self=exclude_self
500
- )
501
-
502
- async def by_id(self, file_id: int | str | FsNode) -> FsNode | None:
503
- """Returns :py:class:`~nc_py_api.files.FsNode` by file_id if any.
504
-
505
- :param file_id: can be full file ID with Nextcloud instance ID or only clear file ID.
506
- """
507
- file_id = file_id.file_id if isinstance(file_id, FsNode) else file_id
508
- result = await self.find(req=["eq", "fileid", file_id])
509
- return result[0] if result else None
510
-
511
- async def by_path(self, path: str | FsNode) -> FsNode | None:
512
- """Returns :py:class:`~nc_py_api.files.FsNode` by exact path if any."""
513
- path = path.user_path if isinstance(path, FsNode) else path
514
- result = await self.listdir(path, depth=0, exclude_self=False)
515
- return result[0] if result else None
516
-
517
- async def find(self, req: list, path: str | FsNode = "") -> list[FsNode]:
518
- """Searches a directory for a file or subdirectory with a name.
519
-
520
- :param req: list of conditions to search for. Detailed description here...
521
- :param path: path where to search from. Default = **""**.
522
- """
523
- # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
524
- root = build_find_request(req, path, await self._session.user)
525
- webdav_response = await self._session.adapter_dav.request(
526
- "SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
527
- )
528
- request_info = f"find: {await self._session.user}, {req}, {path}"
529
- return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
530
-
531
- async def download(self, path: str | FsNode) -> bytes:
532
- """Downloads and returns the content of a file."""
533
- path = path.user_path if isinstance(path, FsNode) else path
534
- response = await self._session.adapter_dav.get(quote(dav_get_obj_path(await self._session.user, path)))
535
- check_error(response, f"download: user={await self._session.user}, path={path}")
536
- return response.content
537
-
538
- async def download2stream(self, path: str | FsNode, fp, **kwargs) -> None:
539
- """Downloads file to the given `fp` object.
540
-
541
- :param path: path to download file.
542
- :param fp: filename (string), pathlib.Path object or a file object.
543
- The object must implement the ``file.write`` method and be able to write binary data.
544
- :param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **5Mb**
545
- """
546
- path = quote(dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path))
547
- await self._session.download2stream(path, fp, dav=True, **kwargs)
548
-
549
- async def download_directory_as_zip(
550
- self, path: str | FsNode, local_path: str | Path | None = None, **kwargs
551
- ) -> Path:
552
- """Downloads a remote directory as zip archive.
553
-
554
- :param path: path to directory to download.
555
- :param local_path: relative or absolute file path to save zip file.
556
- :returns: Path to the saved zip archive.
557
-
558
- .. note:: This works only for directories, you should not use this to download a file.
559
- """
560
- path = path.user_path if isinstance(path, FsNode) else path
561
- result_path = local_path if local_path else os.path.basename(path)
562
- with open(result_path, "wb") as fp:
563
- await self._session.download2fp(
564
- "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
565
- )
566
- return Path(result_path)
567
-
568
- async def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
569
- """Creates a file with the specified content at the specified path.
570
-
571
- :param path: file's upload path.
572
- :param content: content to create the file. If it is a string, it will be encoded into bytes using UTF-8.
573
- """
574
- path = path.user_path if isinstance(path, FsNode) else path
575
- full_path = dav_get_obj_path(await self._session.user, path)
576
- response = await self._session.adapter_dav.put(quote(full_path), content=content)
577
- check_error(response, f"upload: user={await self._session.user}, path={path}, size={len(content)}")
578
- return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
579
-
580
- async def upload_stream(self, path: str | FsNode, fp, **kwargs) -> FsNode:
581
- """Creates a file with content provided by `fp` object at the specified path.
582
-
583
- :param path: file's upload path.
584
- :param fp: filename (string), pathlib.Path object or a file object.
585
- The object must implement the ``file.read`` method providing data with str or bytes type.
586
- :param kwargs: **chunk_size** an int value specifying chunk size to read. Default = **5Mb**
587
- """
588
- path = path.user_path if isinstance(path, FsNode) else path
589
- chunk_size = kwargs.get("chunk_size", 5 * 1024 * 1024)
590
- if isinstance(fp, str | Path):
591
- with builtins.open(fp, "rb") as f:
592
- return await self.__upload_stream(path, f, chunk_size)
593
- elif hasattr(fp, "read"):
594
- return await self.__upload_stream(path, fp, chunk_size)
595
- else:
596
- raise TypeError("`fp` must be a path to file or an object with `read` method.")
597
-
598
- async def mkdir(self, path: str | FsNode) -> FsNode:
599
- """Creates a new directory.
600
-
601
- :param path: path of the directory to be created.
602
- """
603
- path = path.user_path if isinstance(path, FsNode) else path
604
- full_path = dav_get_obj_path(await self._session.user, path)
605
- response = await self._session.adapter_dav.request("MKCOL", quote(full_path))
606
- check_error(response)
607
- full_path += "/" if not full_path.endswith("/") else ""
608
- return FsNode(full_path.lstrip("/"), **etag_fileid_from_response(response))
609
-
610
- async def makedirs(self, path: str | FsNode, exist_ok=False) -> FsNode | None:
611
- """Creates a new directory and subdirectories.
612
-
613
- :param path: path of the directories to be created.
614
- :param exist_ok: ignore error if any of pathname components already exists.
615
- :returns: `FsNode` if directory was created or ``None`` if it was already created.
616
- """
617
- _path = ""
618
- path = path.user_path if isinstance(path, FsNode) else path
619
- path = path.lstrip("/")
620
- result = None
621
- for i in Path(path).parts:
622
- _path = os.path.join(_path, i)
623
- if not exist_ok:
624
- result = await self.mkdir(_path)
625
- else:
626
- try:
627
- result = await self.mkdir(_path)
628
- except NextcloudException as e:
629
- if e.status_code != 405:
630
- raise e from None
631
- return result
632
-
633
- async def delete(self, path: str | FsNode, not_fail=False) -> None:
634
- """Deletes a file/directory (moves to trash if trash is enabled).
635
-
636
- :param path: path to delete.
637
- :param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
638
- """
639
- path = path.user_path if isinstance(path, FsNode) else path
640
- response = await self._session.adapter_dav.delete(quote(dav_get_obj_path(await self._session.user, path)))
641
- if response.status_code == 404 and not_fail:
642
- return
643
- check_error(response)
644
-
645
- async def move(self, path_src: str | FsNode, path_dest: str | FsNode, overwrite=False) -> FsNode:
646
- """Moves an existing file or a directory.
647
-
648
- :param path_src: path of an existing file/directory.
649
- :param path_dest: name of the new one.
650
- :param overwrite: if ``True`` and the destination object already exists, it gets overwritten.
651
- Default = **False**.
652
- """
653
- path_src = path_src.user_path if isinstance(path_src, FsNode) else path_src
654
- full_dest_path = dav_get_obj_path(
655
- await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
656
- )
657
- dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
658
- headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
659
- response = await self._session.adapter_dav.request(
660
- "MOVE",
661
- quote(dav_get_obj_path(await self._session.user, path_src)),
662
- headers=headers,
663
- )
664
- check_error(response, f"move: user={await self._session.user}, src={path_src}, dest={dest}, {overwrite}")
665
- return (await self.find(req=["eq", "fileid", response.headers["OC-FileId"]]))[0]
666
-
667
- async def copy(self, path_src: str | FsNode, path_dest: str | FsNode, overwrite=False) -> FsNode:
668
- """Copies an existing file/directory.
669
-
670
- :param path_src: path of an existing file/directory.
671
- :param path_dest: name of the new one.
672
- :param overwrite: if ``True`` and the destination object already exists, it gets overwritten.
673
- Default = **False**.
674
- """
675
- path_src = path_src.user_path if isinstance(path_src, FsNode) else path_src
676
- full_dest_path = dav_get_obj_path(
677
- await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
678
- )
679
- dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
680
- headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
681
- response = await self._session.adapter_dav.request(
682
- "COPY",
683
- quote(dav_get_obj_path(await self._session.user, path_src)),
684
- headers=headers,
685
- )
686
- check_error(response, f"copy: user={await self._session.user}, src={path_src}, dest={dest}, {overwrite}")
687
- return (await self.find(req=["eq", "fileid", response.headers["OC-FileId"]]))[0]
688
-
689
- async def list_by_criteria(
690
- self, properties: list[str] | None = None, tags: list[int | SystemTag] | None = None
691
- ) -> list[FsNode]:
692
- """Returns a list of all files/directories for the current user filtered by the specified values.
693
-
694
- :param properties: List of ``properties`` that should have been set for the file.
695
- Supported values: **favorite**
696
- :param tags: List of ``tags ids`` or ``SystemTag`` that should have been set for the file.
697
- """
698
- root = build_list_by_criteria_req(properties, tags)
699
- webdav_response = await self._session.adapter_dav.request(
700
- "REPORT", dav_get_obj_path(await self._session.user), content=element_tree_as_str(root)
701
- )
702
- request_info = f"list_files_by_criteria: {await self._session.user}"
703
- check_error(webdav_response, request_info)
704
- return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
705
-
706
- async def setfav(self, path: str | FsNode, value: int | bool) -> None:
707
- """Sets or unsets favourite flag for specific file.
708
-
709
- :param path: path to the object to set the state.
710
- :param value: value to set for the ``favourite`` state.
711
- """
712
- path = path.user_path if isinstance(path, FsNode) else path
713
- root = build_setfav_req(value)
714
- webdav_response = await self._session.adapter_dav.request(
715
- "PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)), content=element_tree_as_str(root)
716
- )
717
- check_error(webdav_response, f"setfav: path={path}, value={value}")
718
-
719
- async def trashbin_list(self) -> list[FsNode]:
720
- """Returns a list of all entries in the TrashBin."""
721
- properties = PROPFIND_PROPERTIES
722
- properties += ["nc:trashbin-filename", "nc:trashbin-original-location", "nc:trashbin-deletion-time"]
723
- return await self._listdir(
724
- await self._session.user,
725
- "",
726
- properties=properties,
727
- depth=1,
728
- exclude_self=False,
729
- prop_type=PropFindType.TRASHBIN,
730
- )
731
-
732
- async def trashbin_restore(self, path: str | FsNode) -> None:
733
- """Restore a file/directory from the TrashBin.
734
-
735
- :param path: path to delete, e.g., the ``user_path`` field from ``FsNode`` or the **FsNode** class itself.
736
- """
737
- restore_name = path.name if isinstance(path, FsNode) else path.split("/", maxsplit=1)[-1]
738
- path = path.user_path if isinstance(path, FsNode) else path
739
-
740
- dest = self._session.cfg.dav_endpoint + f"/trashbin/{await self._session.user}/restore/{restore_name}"
741
- headers = Headers({"Destination": dest}, encoding="utf-8")
742
- response = await self._session.adapter_dav.request(
743
- "MOVE",
744
- quote(f"/trashbin/{await self._session.user}/{path}"),
745
- headers=headers,
746
- )
747
- check_error(response, f"trashbin_restore: user={await self._session.user}, src={path}, dest={dest}")
748
-
749
- async def trashbin_delete(self, path: str | FsNode, not_fail=False) -> None:
750
- """Deletes a file/directory permanently from the TrashBin.
751
-
752
- :param path: path to delete, e.g., the ``user_path`` field from ``FsNode`` or the **FsNode** class itself.
753
- :param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
754
- """
755
- path = path.user_path if isinstance(path, FsNode) else path
756
- response = await self._session.adapter_dav.delete(quote(f"/trashbin/{await self._session.user}/{path}"))
757
- if response.status_code == 404 and not_fail:
758
- return
759
- check_error(response)
760
-
761
- async def trashbin_cleanup(self) -> None:
762
- """Empties the TrashBin."""
763
- check_error(await self._session.adapter_dav.delete(f"/trashbin/{await self._session.user}/trash"))
764
-
765
- async def get_versions(self, file_object: FsNode) -> list[FsNode]:
766
- """Returns a list of all file versions if any."""
767
- require_capabilities("files.versioning", await self._session.capabilities)
768
- return await self._listdir(
769
- await self._session.user,
770
- str(file_object.info.fileid) if file_object.info.fileid else file_object.file_id,
771
- properties=PROPFIND_PROPERTIES,
772
- depth=1,
773
- exclude_self=False,
774
- prop_type=PropFindType.VERSIONS_FILEID if file_object.info.fileid else PropFindType.VERSIONS_FILE_ID,
775
- )
776
-
777
- async def restore_version(self, file_object: FsNode) -> None:
778
- """Restore a file with specified version.
779
-
780
- :param file_object: The **FsNode** class from :py:meth:`~nc_py_api.files.files.FilesAPI.get_versions`.
781
- """
782
- require_capabilities("files.versioning", await self._session.capabilities)
783
- dest = self._session.cfg.dav_endpoint + f"/versions/{await self._session.user}/restore/{file_object.name}"
784
- headers = Headers({"Destination": dest}, encoding="utf-8")
785
- response = await self._session.adapter_dav.request(
786
- "MOVE",
787
- f"/versions/{await self._session.user}/{file_object.user_path}",
788
- headers=headers,
789
- )
790
- check_error(response, f"restore_version: user={await self._session.user}, src={file_object.user_path}")
791
-
792
- async def list_tags(self) -> list[SystemTag]:
793
- """Returns list of the avalaible Tags."""
794
- root = build_list_tag_req()
795
- response = await self._session.adapter_dav.request("PROPFIND", "/systemtags", content=element_tree_as_str(root))
796
- return build_list_tags_response(response)
797
-
798
- async def create_tag(self, name: str, user_visible: bool = True, user_assignable: bool = True) -> None:
799
- """Creates a new Tag.
800
-
801
- :param name: Name of the tag.
802
- :param user_visible: Should be Tag visible in the UI.
803
- :param user_assignable: Can Tag be assigned from the UI.
804
- """
805
- response = await self._session.adapter_dav.post(
806
- "/systemtags",
807
- json={
808
- "name": name,
809
- "userVisible": user_visible,
810
- "userAssignable": user_assignable,
811
- },
812
- )
813
- check_error(response, info=f"create_tag({name})")
814
-
815
- async def update_tag(
816
- self,
817
- tag_id: int | SystemTag,
818
- name: str | None = None,
819
- user_visible: bool | None = None,
820
- user_assignable: bool | None = None,
821
- ) -> None:
822
- """Updates the Tag information."""
823
- tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
824
- root = build_update_tag_req(name, user_visible, user_assignable)
825
- response = await self._session.adapter_dav.request(
826
- "PROPPATCH", f"/systemtags/{tag_id}", content=element_tree_as_str(root)
827
- )
828
- check_error(response)
829
-
830
- async def delete_tag(self, tag_id: int | SystemTag) -> None:
831
- """Deletes the tag."""
832
- tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
833
- response = await self._session.adapter_dav.delete(f"/systemtags/{tag_id}")
834
- check_error(response)
835
-
836
- async def tag_by_name(self, tag_name: str) -> SystemTag:
837
- """Returns Tag info by its name if found or ``None`` otherwise."""
838
- r = [i for i in await self.list_tags() if i.display_name == tag_name]
839
- if not r:
840
- raise NextcloudExceptionNotFound(f"Tag with name='{tag_name}' not found.")
841
- return r[0]
842
-
843
- async def assign_tag(self, file_id: FsNode | int, tag_id: SystemTag | int) -> None:
844
- """Assigns Tag to a file/directory."""
845
- await self._file_change_tag_state(file_id, tag_id, True)
846
-
847
- async def unassign_tag(self, file_id: FsNode | int, tag_id: SystemTag | int) -> None:
848
- """Removes Tag from a file/directory."""
849
- await self._file_change_tag_state(file_id, tag_id, False)
850
-
851
- async def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
852
- fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
853
- tag = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
854
- response = await self._session.adapter_dav.request(
855
- "PUT" if tag_state else "DELETE", f"/systemtags-relations/files/{fs_object}/{tag}"
856
- )
857
- check_error(
858
- response,
859
- info=f"({'Adding' if tag_state else 'Removing'} `{tag}` {'to' if tag_state else 'from'} {fs_object})",
860
- )
861
-
862
- async def _listdir(
863
- self,
864
- user: str,
865
- path: str,
866
- properties: list[str],
867
- depth: int,
868
- exclude_self: bool,
869
- prop_type: PropFindType = PropFindType.DEFAULT,
870
- ) -> list[FsNode]:
871
- root, dav_path = build_listdir_req(user, path, properties, prop_type)
872
- webdav_response = await self._session.adapter_dav.request(
873
- "PROPFIND",
874
- quote(dav_path),
875
- content=element_tree_as_str(root),
876
- headers={"Depth": "infinity" if depth == -1 else str(depth)},
877
- )
878
- return build_listdir_response(
879
- self._session.cfg.dav_url_suffix, webdav_response, user, path, properties, exclude_self, prop_type
880
- )
881
-
882
- async def __upload_stream(self, path: str, fp, chunk_size: int) -> FsNode:
883
- _tmp_path = "nc-py-api-" + random_string(56)
884
- _dav_path = quote(dav_get_obj_path(await self._session.user, _tmp_path, root_path="/uploads"))
885
- _v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
886
- full_path = dav_get_obj_path(await self._session.user, path)
887
- headers = Headers({"Destination": self._session.cfg.dav_endpoint + quote(full_path)}, encoding="utf-8")
888
- if _v2:
889
- response = await self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
890
- else:
891
- response = await self._session.adapter_dav.request("MKCOL", _dav_path)
892
- check_error(response)
893
- try:
894
- start_bytes = end_bytes = chunk_number = 0
895
- while True:
896
- piece = fp.read(chunk_size)
897
- if not piece:
898
- break
899
- end_bytes = start_bytes + len(piece)
900
- if _v2:
901
- response = await self._session.adapter_dav.put(
902
- _dav_path + "/" + str(chunk_number), content=piece, headers=headers
903
- )
904
- else:
905
- _filename = str(start_bytes).rjust(15, "0") + "-" + str(end_bytes).rjust(15, "0")
906
- response = await self._session.adapter_dav.put(_dav_path + "/" + _filename, content=piece)
907
- check_error(
908
- response,
909
- f"upload_stream(v={_v2}): user={await self._session.user}, path={path}, cur_size={end_bytes}",
910
- )
911
- start_bytes = end_bytes
912
- chunk_number += 1
913
-
914
- response = await self._session.adapter_dav.request(
915
- "MOVE",
916
- _dav_path + "/.file",
917
- headers=headers,
918
- )
919
- check_error(
920
- response,
921
- f"upload_stream(v={_v2}): user={await self._session.user}, path={path}, total_size={end_bytes}",
922
- )
923
- return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
924
- finally:
925
- await self._session.adapter_dav.delete(_dav_path)