Skip to content

Commit

Permalink
download2stream implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
bigcat88 committed Jul 11, 2023
1 parent 3d01c2b commit 2d4a913
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.

- `VERIFY_NC_CERTIFICATE` option.
- `apps.ex_app_get_list` and `apps.ex_app_get_info` methods.
- `files.download2stream` method.

## [0.0.23 - 2023-07-07]

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Have a great time with Python and Nextcloud!
.. toctree::
:maxdepth: 1

reference/index.rst
benchmarks/AppEcosystem.rst

Indices and tables
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/Files.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File System
===========

The Files API is universal for both modes and provides all the necessary methods for working with the Nextcloud file system.
Refer to the **fs examples** to see how to use them nicely.

.. autoclass:: nc_py_api.files.FilesAPI
:members:
7 changes: 7 additions & 0 deletions docs/reference/Session.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Sessions Internal
=================

Currently Session API is private, and not exposed.

.. autoclass:: nc_py_api._session.NcSessionBasic
:members:
8 changes: 8 additions & 0 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Reference
=========

.. toctree::
:maxdepth: 2

Files.rst
Session.rst
26 changes: 24 additions & 2 deletions nc_py_api/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import asyncio
import hmac
from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timezone
from hashlib import sha256
from json import dumps, loads
from os import environ
from typing import Optional, TypedDict, Union
from typing import Iterator, Optional, TypedDict, Union
from urllib.parse import quote, urlencode

from fastapi import Request
Expand Down Expand Up @@ -152,20 +153,37 @@ def _ocs(self, method: str, path_params: str, headers: dict, data: Optional[byte
raise NextcloudException(status_code=ocs_meta["statuscode"], reason=ocs_meta["message"], info=info)
return response_data["ocs"]["data"]

def dav(self, method: str, path: str, data: Optional[Union[str, bytes]] = None, **kwargs):
def dav(self, method: str, path: str, data: Optional[Union[str, bytes]] = None, **kwargs) -> Response:
headers = kwargs.pop("headers", {})
data_bytes = None
if data is not None:
data_bytes = data.encode("UTF-8") if isinstance(data, str) else data
return self._dav(method, quote(self.cfg.dav_url_suffix + path), headers, data_bytes, **kwargs)

@contextmanager
def dav_stream(
self, method: str, path: str, data: Optional[Union[str, bytes]] = None, **kwargs
) -> Iterator[Response]:
headers = kwargs.pop("headers", {})
data_bytes = None
if data is not None:
data_bytes = data.encode("UTF-8") if isinstance(data, str) else data
return self._dav_stream(method, quote(self.cfg.dav_url_suffix + path), headers, data_bytes, **kwargs)

def _dav(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Response:
self.init_adapter()
timeout = kwargs.pop("timeout", options.TIMEOUT_DAV)
return self.adapter.request(
method, self.cfg.endpoint + path, headers=headers, content=data, timeout=timeout, **kwargs
)

def _dav_stream(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Iterator[Response]:
self.init_adapter()
timeout = kwargs.pop("timeout", options.TIMEOUT_DAV)
return self.adapter.stream(
method, self.cfg.endpoint + path, headers=headers, content=data, timeout=timeout, **kwargs
)

def init_adapter(self, restart=False) -> None:
if getattr(self, "adapter", None) is None or restart:
if restart and hasattr(self, "adapter"):
Expand Down Expand Up @@ -233,6 +251,10 @@ def _dav(self, method: str, path: str, headers: dict, data: Optional[bytes], **k
self.sign_request(method, path, headers, data)
return super()._dav(method, path, headers, data, **kwargs)

def _dav_stream(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Iterator[Response]:
self.sign_request(method, path, headers, data)
return super()._dav_stream(method, path, headers, data, **kwargs)

def _create_adapter(self) -> Client:
adapter = Client(follow_redirects=True, limits=self.limits, verify=options.VERIFY_NC_CERTIFICATE)
adapter.headers.update(
Expand Down
2 changes: 1 addition & 1 deletion nc_py_api/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
""" Version of nc_py_api"""

__version__ = "0.0.23"
__version__ = "0.0.24"
19 changes: 19 additions & 0 deletions nc_py_api/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,29 @@ def find(self, req: list, path="", depth=-1) -> list[FsNode]:
return self._lf_parse_webdav_records(webdav_response, self._session.user, request_info)

def download(self, path: str) -> bytes:
"""Downloads and returns the contents of a file.
:param path: Path to a file to download relative to root directory of the user.
"""

response = self._session.dav("GET", self._dav_get_obj_path(self._session.user, path))
check_error(response.status_code, f"download: user={self._session.user}, path={path}")
return response.content

def download2stream(self, path: str, fp, **kwargs) -> None:
"""Downloads file to the given `fp` object.
:param path: Path to a file to download relative to root directory of the user.
:param fp: A filename (string), pathlib.Path object or a file object.
The object must implement the ``file.write`` method and be able to write binary data.
:param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **512Kb**
"""

with self._session.dav_stream("GET", self._dav_get_obj_path(self._session.user, path)) as response:
check_error(response.status_code, f"download: user={self._session.user}, path={path}")
for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 512 * 1024)):
fp.write(data_chunk)

def upload(self, path: str, content: Union[bytes, str]) -> None:
response = self._session.dav("PUT", self._dav_get_obj_path(self._session.user, path), data=content)
check_error(response.status_code, f"upload: user={self._session.user}, path={path}, size={len(content)}")
Expand Down
32 changes: 31 additions & 1 deletion tests/files_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,43 @@ def test_file_download(nc):


@pytest.mark.parametrize("nc", NC_TO_TEST)
def test_file_not_found(nc):
def test_file_download2stream(nc):
class MyBytesIO(BytesIO):
def __init__(self):
self.n_calls = 0
super().__init__()

def write(self, content):
self.n_calls += 1
super().write(content)

srv_admin_manual1_buf = MyBytesIO()
srv_admin_manual2_buf = MyBytesIO()
nc.files.upload("test_file.txt", content=randbytes(64))
nc.files.download2stream("test_file.txt", srv_admin_manual1_buf)
nc.files.download2stream("/test_file.txt", srv_admin_manual2_buf, chunk_size=16)
assert srv_admin_manual1_buf.getbuffer() == srv_admin_manual2_buf.getbuffer()
assert srv_admin_manual1_buf.n_calls == 1
assert srv_admin_manual2_buf.n_calls == 4


@pytest.mark.parametrize("nc", NC_TO_TEST)
def test_file_download_not_found(nc):
with pytest.raises(NextcloudException):
nc.files.download("file that does not exist on the server")
with pytest.raises(NextcloudException):
nc.files.listdir("non existing path")


@pytest.mark.parametrize("nc", NC_TO_TEST)
def test_file_download2stream_not_found(nc):
buf = BytesIO()
with pytest.raises(NextcloudException):
nc.files.download2stream("file that does not exist on the server", buf)
with pytest.raises(NextcloudException):
nc.files.download2stream("non existing path", buf)


@pytest.mark.parametrize("nc", NC_TO_TEST)
def test_file_upload(nc):
file_name = "12345.txt"
Expand Down

0 comments on commit 2d4a913

Please sign in to comment.