From 0fe4a5b6201cca7f14811572bd0e87e63d6b6b02 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 27 Aug 2024 21:45:43 -0300 Subject: [PATCH 01/88] feat: Resumable uploads --- storage3/types.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/storage3/types.py b/storage3/types.py index 1d9441cc..96389227 100644 --- a/storage3/types.py +++ b/storage3/types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from datetime import datetime -from typing import Optional, Union +from typing import Dict, Optional, Union import dateutil.parser from typing_extensions import Literal, TypedDict @@ -80,3 +80,16 @@ class DownloadOptions(TypedDict, total=False): {"cache-control": str, "content-type": str, "x-upsert": str, "upsert": str}, total=False, ) + + +class UploadMetadata(TypedDict): + bucketName: str + objectName: str + + +class FileInfo(TypedDict): + name: str + link: str + length: str + headers: Dict[str, str] + expiration_time: float From ef3f47922bd0e1ea776ab6f6a08025a800131fcb Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 27 Aug 2024 21:50:49 -0300 Subject: [PATCH 02/88] feat: Resumable uploads --- storage3/resumable.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 storage3/resumable.py diff --git a/storage3/resumable.py b/storage3/resumable.py new file mode 100644 index 00000000..6be490c3 --- /dev/null +++ b/storage3/resumable.py @@ -0,0 +1,11 @@ +import os +from base64 import b64encode +from datetime import datetime + +from .types import FileInfo, UploadMetadata + + +# __all__ = [] + + +# WIP !!! From 0e064e665e924ed8c04f7c65528227b5574231b4 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 14:26:20 -0300 Subject: [PATCH 03/88] feat: resumable progress wip --- storage3/resumable.py | 210 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 2 deletions(-) diff --git a/storage3/resumable.py b/storage3/resumable.py index 6be490c3..d1425444 100644 --- a/storage3/resumable.py +++ b/storage3/resumable.py @@ -3,9 +3,215 @@ from datetime import datetime from .types import FileInfo, UploadMetadata +from .utils import StorageException, SyncClient -# __all__ = [] +__all__ = ["ResumableUpload"] -# WIP !!! +class FileStore: + """This class serves as storage of files to be sent in the resumable upload workflow""" + + def __init__(self): + self.storage = {} + + def mark_file(self, file_info: FileInfo): + """Store file metadata in a in-memory storage""" + self.storage[file_info["name"]] = file_info + + def get_file_info(self, filename): + return self.storage[filename] + + def update_file_headers(self, filename, key, value): + file = self.get_file_info(filename) + file["headers"][key] = value + + def get_file_headers(self, filename): + return self.get_file_info(filename)["headers"] + + def get_file_storage_link(self, filename): + return self.get_file_info(filename)["headers"]["link"] + + def open_file(self, filename: str, offset: int): + """Open file in the specified offset + Parameters + ---------- + filename + local file + offset + set current the file-pointer + """ + file = open(filename, "rb") + file.seek(offset) + return file + + def close_file(self, filename): + filename.close() + + def remove_file(self, filename: str): + del self.storage[filename] + + def get_link(self, filename: str): + return self.storage[filename]["link"] + + +class ResumableUpload: + def __init__(self, session: SyncClient) -> None: + self._client = session + self.url = f"{self._client.base_url}upload/resumable" + self.expiration_time_format = "%a, %d %b %Y %X %Z" + self._filestore = FileStore() + + def _encode(self, metadata: UploadMetadata) -> str: + """Generate base64 encoding for Upload-Metadata header + Parameters + ---------- + metadata + Bucket and object pair representing the resulting file in the storage + """ + res = [ + f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items() + ] + return ",".join(res) + + def file_exists(self, filename) -> bool: + """Verify if the file exists in the storage + Parameters + ---------- + filename + This could be the local filename or objectname in the storage + """ + return filename in self._filestore.storage + + def get_link(self, objectname) -> str: + """Get the link associated with objectname in the bucket + Parameters + ---------- + objectname + This could be the local filename or objectname in the storage + """ + if not self.file_exists(objectname): + raise StorageException(f"There is no entry for {objectname} in FileStore") + return self._filestore.get_link(objectname) + + def create_unique_link( + self, bucketname=None, objectname=None, filename=None + ) -> None: + """Create unique link according to bucketname and objectname + Parameters + ---------- + bucketname + Storage bucket + objectname + Filename in the bucket + filename + Local file + """ + if bucketname is None: + raise StorageException("bucketname cannot be empty") + + if objectname is None and filename is None: + raise StorageException("Must specify objectname or filename") + + file = None + upload_mode = None + + if filename: + _, file = os.path.split(filename) + else: + file = objectname + + info = FileInfo( + name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} + ) + + if not filename: + upload_mode = "Upload-Defer-Length" + info["headers"][upload_mode] = "1" + else: + upload_mode = "Upload-Length" + size = str(os.stat(filename).st_size) + info["headers"][upload_mode] = size + info["length"] = size + + metadata = UploadMetadata(bucketName=bucketname, objectName=file) + + info["headers"]["Upload-Metadata"] = self._encode(metadata) + response = self._client.post(self.url, headers=info["headers"]) + + if response.status_code != 201: + raise StorageException(response.content) + + expiration_time = datetime.strptime( + response.headers["upload-expires"], self.expiration_time_format + ) + info["expiration_time"] = expiration_time.timestamp() + + info["link"] = response.headers["location"] + del info["headers"][upload_mode] + self._filestore.mark_file(info) + + def resumable_offset(self, link, headers) -> str: + """Get the current offset to be used + Parameters + ---------- + link + Target url + headers + Metadata headers sent to the server + """ + response = self._client.head(link, headers=headers) + return response.headers["upload-offset"] + + def upload( + self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 + ) -> None: + """Send file's content in chunks to the target url + Parameters + ---------- + filename + Local file + upload_defer + Requires link and objectname to be True to retrieve file info in the FileStore + link + Target url + objectname + Name of the file in the bucket + mb_size + Amount of megabytes to be sent in each iteration + """ + if upload_defer: + if link is None or objectname is None: + raise StorageException( + "Upload-Defer mode requires a link and objectname" + ) + + target_file = objectname if upload_defer else os.path.split(filename)[1] + chunk_size = 1048576 * mb_size + size = None + self._filestore.update_file_headers( + target_file, "Content-Type", "application/offset+octet-stream" + ) + storage_link = link if upload_defer else self._filestore.get_link(target_file) + + if upload_defer: + size = str(os.stat(filename).st_size) + self._filestore.update_file_headers(target_file, "Upload-Length", size) + + while True: + headers = self._filestore.get_file_headers(target_file) + offset = self.resumable_offset(storage_link, headers) + file = self._filestore.open_file(filename, offset=int(offset)) + self._filestore.update_file_headers(target_file, "Upload-Offset", offset) + + chunk = file.read(chunk_size) + headers = self._filestore.get_file_headers(target_file) + response = self._client.patch(storage_link, headers=headers, data=chunk) + + if response.status_code not in {201, 204}: + raise StorageException(response.content) + + if "tus-complete" in response.headers: + self._filestore.close_file(file) + self._filestore.remove_file(target_file) + break From 3d42e60c012069d573b8b68b5a6a2e59eebfdf01 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 14:29:52 -0300 Subject: [PATCH 04/88] feat: resumable progress wip --- storage3/_sync/bucket.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index 5780776c..328f84ce 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -4,10 +4,12 @@ from httpx import HTTPError, Response +from ..resumable import ResumableUpload from ..types import CreateOrUpdateBucketOptions, RequestMethod from ..utils import StorageException, SyncClient from .file_api import SyncBucket + __all__ = ["SyncStorageBucketAPI"] @@ -16,6 +18,7 @@ class SyncStorageBucketAPI: def __init__(self, session: SyncClient) -> None: self._client = session + self._resumable = None def _request( self, @@ -33,6 +36,13 @@ def _request( return response + @property + def resumable(self): + if self._resumable is None: + self._resumable = ResumableUpload(self._client) + + return self._resumable + def list_buckets(self) -> list[SyncBucket]: """Retrieves the details of all storage buckets within an existing product.""" # if the request doesn't error, it is assured to return a list From a46d2c21d120dc8c908f0a21823bfe9e401bdea1 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 14:31:18 -0300 Subject: [PATCH 05/88] fix: code styles --- storage3/resumable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/storage3/resumable.py b/storage3/resumable.py index d1425444..deb0bfd6 100644 --- a/storage3/resumable.py +++ b/storage3/resumable.py @@ -5,7 +5,6 @@ from .types import FileInfo, UploadMetadata from .utils import StorageException, SyncClient - __all__ = ["ResumableUpload"] From 2e488b50342fc13de09f1fb4b92811f9fe077d4c Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 14:31:31 -0300 Subject: [PATCH 06/88] fix: code styles --- storage3/_sync/bucket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index 328f84ce..545c4f1e 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -9,7 +9,6 @@ from ..utils import StorageException, SyncClient from .file_api import SyncBucket - __all__ = ["SyncStorageBucketAPI"] From 45e3554ca99f947e1eb67b145635b3130039a385 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 14:54:48 -0300 Subject: [PATCH 07/88] fix: code styles --- storage3/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/resumable.py b/storage3/resumable.py index deb0bfd6..01f341e3 100644 --- a/storage3/resumable.py +++ b/storage3/resumable.py @@ -186,7 +186,7 @@ def upload( ) target_file = objectname if upload_defer else os.path.split(filename)[1] - chunk_size = 1048576 * mb_size + chunk_size = 1048576 * int(mb_size) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" From d923bb69921af9b5322f37373b03dc65a3492cfe Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 15:09:04 -0300 Subject: [PATCH 08/88] fix: code styles --- storage3/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/resumable.py b/storage3/resumable.py index 01f341e3..181074c2 100644 --- a/storage3/resumable.py +++ b/storage3/resumable.py @@ -186,7 +186,7 @@ def upload( ) target_file = objectname if upload_defer else os.path.split(filename)[1] - chunk_size = 1048576 * int(mb_size) # 1024 * 1024 * mb_size + chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" From 87ab99d346522cbf978c72b138fab067bfb59533 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 15:24:42 -0300 Subject: [PATCH 09/88] fix: code styles --- storage3/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/resumable.py b/storage3/resumable.py index 181074c2..07945087 100644 --- a/storage3/resumable.py +++ b/storage3/resumable.py @@ -5,7 +5,7 @@ from .types import FileInfo, UploadMetadata from .utils import StorageException, SyncClient -__all__ = ["ResumableUpload"] +__all__ = ("ResumableUpload", ) class FileStore: From 313cfb76d3245a012253fb4c342889afc6da3d12 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 4 Sep 2024 15:31:46 -0300 Subject: [PATCH 10/88] fix: code styles --- storage3/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/resumable.py b/storage3/resumable.py index 07945087..8c45d454 100644 --- a/storage3/resumable.py +++ b/storage3/resumable.py @@ -5,7 +5,7 @@ from .types import FileInfo, UploadMetadata from .utils import StorageException, SyncClient -__all__ = ("ResumableUpload", ) +__all__ = ("ResumableUpload",) class FileStore: From f7bfdb07ebca4f8a0d7e4e2045d50f840f1364f4 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 9 Sep 2024 18:30:00 -0300 Subject: [PATCH 11/88] feat: progress --- storage3/_async/bucket.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/storage3/_async/bucket.py b/storage3/_async/bucket.py index 187f82b7..04609c2d 100644 --- a/storage3/_async/bucket.py +++ b/storage3/_async/bucket.py @@ -7,8 +7,10 @@ from ..types import CreateOrUpdateBucketOptions, RequestMethod from ..utils import AsyncClient, StorageException from .file_api import AsyncBucket +from .resumable import AsyncResumableUpload -__all__ = ["AsyncStorageBucketAPI"] + +__all__ = ("AsyncStorageBucketAPI", ) class AsyncStorageBucketAPI: @@ -16,6 +18,13 @@ class AsyncStorageBucketAPI: def __init__(self, session: AsyncClient) -> None: self._client = session + self._resumable = None + + @property + def resumable(self): + if self._resumable is None: + self._resumable = AsyncResumableUpload(self._client) + return self._resumable async def _request( self, From 276badaf31a2f116c4848f9e4288f95ef9ed3b0d Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 9 Sep 2024 18:30:06 -0300 Subject: [PATCH 12/88] feat: progress --- storage3/_sync/bucket.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index 545c4f1e..cda34b42 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -8,8 +8,10 @@ from ..types import CreateOrUpdateBucketOptions, RequestMethod from ..utils import StorageException, SyncClient from .file_api import SyncBucket +from .resumable import ResumableUpload -__all__ = ["SyncStorageBucketAPI"] + +__all__ = ("SyncStorageBucketAPI", ) class SyncStorageBucketAPI: @@ -19,6 +21,13 @@ def __init__(self, session: SyncClient) -> None: self._client = session self._resumable = None + @property + def resumable(self): + if self._resumable is None: + self._resumable = ResumableUpload(self._client) + + return self._resumable + def _request( self, method: RequestMethod, @@ -35,13 +44,6 @@ def _request( return response - @property - def resumable(self): - if self._resumable is None: - self._resumable = ResumableUpload(self._client) - - return self._resumable - def list_buckets(self) -> list[SyncBucket]: """Retrieves the details of all storage buckets within an existing product.""" # if the request doesn't error, it is assured to return a list From 3989e4b7cf1ef0485b43aef7037a1c96ed349711 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 9 Sep 2024 18:30:23 -0300 Subject: [PATCH 13/88] feat: progress --- storage3/utils.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/storage3/utils.py b/storage3/utils.py index 9689fc8e..c1ffb569 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -1,6 +1,8 @@ from httpx import AsyncClient as AsyncClient # noqa: F401 from httpx import Client as BaseClient +from .types import FileInfo + class SyncClient(BaseClient): def aclose(self) -> None: @@ -9,3 +11,49 @@ def aclose(self) -> None: class StorageException(Exception): """Error raised when an operation on the storage API fails.""" + + +class FileStore: + """This class serves as storage of files to be sent in the resumable upload workflow""" + + def __init__(self): + self.storage = {} + + def mark_file(self, file_info: FileInfo): + """Store file metadata in a in-memory storage""" + self.storage[file_info["name"]] = file_info + + def get_file_info(self, filename): + return self.storage[filename] + + def update_file_headers(self, filename, key, value): + file = self.get_file_info(filename) + file["headers"][key] = value + + def get_file_headers(self, filename): + return self.get_file_info(filename)["headers"] + + def get_file_storage_link(self, filename): + return self.get_file_info(filename)["headers"]["link"] + + def open_file(self, filename: str, offset: int): + """Open file in the specified offset + Parameters + ---------- + filename + local file + offset + set current the file-pointer + """ + file = open(filename, "rb") + file.seek(int(offset)) + return file + + def close_file(self, filename): + filename.close() + + def remove_file(self, filename: str): + del self.storage[filename] + + def get_link(self, filename: str): + return self.storage[filename]["link"] From 51e78c39f5c1f77cb544ac397b92035abd0d181f Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 9 Sep 2024 18:41:33 -0300 Subject: [PATCH 14/88] feat: More progress --- storage3/resumable.py | 216 ------------------------------------------ 1 file changed, 216 deletions(-) delete mode 100644 storage3/resumable.py diff --git a/storage3/resumable.py b/storage3/resumable.py deleted file mode 100644 index 8c45d454..00000000 --- a/storage3/resumable.py +++ /dev/null @@ -1,216 +0,0 @@ -import os -from base64 import b64encode -from datetime import datetime - -from .types import FileInfo, UploadMetadata -from .utils import StorageException, SyncClient - -__all__ = ("ResumableUpload",) - - -class FileStore: - """This class serves as storage of files to be sent in the resumable upload workflow""" - - def __init__(self): - self.storage = {} - - def mark_file(self, file_info: FileInfo): - """Store file metadata in a in-memory storage""" - self.storage[file_info["name"]] = file_info - - def get_file_info(self, filename): - return self.storage[filename] - - def update_file_headers(self, filename, key, value): - file = self.get_file_info(filename) - file["headers"][key] = value - - def get_file_headers(self, filename): - return self.get_file_info(filename)["headers"] - - def get_file_storage_link(self, filename): - return self.get_file_info(filename)["headers"]["link"] - - def open_file(self, filename: str, offset: int): - """Open file in the specified offset - Parameters - ---------- - filename - local file - offset - set current the file-pointer - """ - file = open(filename, "rb") - file.seek(offset) - return file - - def close_file(self, filename): - filename.close() - - def remove_file(self, filename: str): - del self.storage[filename] - - def get_link(self, filename: str): - return self.storage[filename]["link"] - - -class ResumableUpload: - def __init__(self, session: SyncClient) -> None: - self._client = session - self.url = f"{self._client.base_url}upload/resumable" - self.expiration_time_format = "%a, %d %b %Y %X %Z" - self._filestore = FileStore() - - def _encode(self, metadata: UploadMetadata) -> str: - """Generate base64 encoding for Upload-Metadata header - Parameters - ---------- - metadata - Bucket and object pair representing the resulting file in the storage - """ - res = [ - f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items() - ] - return ",".join(res) - - def file_exists(self, filename) -> bool: - """Verify if the file exists in the storage - Parameters - ---------- - filename - This could be the local filename or objectname in the storage - """ - return filename in self._filestore.storage - - def get_link(self, objectname) -> str: - """Get the link associated with objectname in the bucket - Parameters - ---------- - objectname - This could be the local filename or objectname in the storage - """ - if not self.file_exists(objectname): - raise StorageException(f"There is no entry for {objectname} in FileStore") - return self._filestore.get_link(objectname) - - def create_unique_link( - self, bucketname=None, objectname=None, filename=None - ) -> None: - """Create unique link according to bucketname and objectname - Parameters - ---------- - bucketname - Storage bucket - objectname - Filename in the bucket - filename - Local file - """ - if bucketname is None: - raise StorageException("bucketname cannot be empty") - - if objectname is None and filename is None: - raise StorageException("Must specify objectname or filename") - - file = None - upload_mode = None - - if filename: - _, file = os.path.split(filename) - else: - file = objectname - - info = FileInfo( - name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} - ) - - if not filename: - upload_mode = "Upload-Defer-Length" - info["headers"][upload_mode] = "1" - else: - upload_mode = "Upload-Length" - size = str(os.stat(filename).st_size) - info["headers"][upload_mode] = size - info["length"] = size - - metadata = UploadMetadata(bucketName=bucketname, objectName=file) - - info["headers"]["Upload-Metadata"] = self._encode(metadata) - response = self._client.post(self.url, headers=info["headers"]) - - if response.status_code != 201: - raise StorageException(response.content) - - expiration_time = datetime.strptime( - response.headers["upload-expires"], self.expiration_time_format - ) - info["expiration_time"] = expiration_time.timestamp() - - info["link"] = response.headers["location"] - del info["headers"][upload_mode] - self._filestore.mark_file(info) - - def resumable_offset(self, link, headers) -> str: - """Get the current offset to be used - Parameters - ---------- - link - Target url - headers - Metadata headers sent to the server - """ - response = self._client.head(link, headers=headers) - return response.headers["upload-offset"] - - def upload( - self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 - ) -> None: - """Send file's content in chunks to the target url - Parameters - ---------- - filename - Local file - upload_defer - Requires link and objectname to be True to retrieve file info in the FileStore - link - Target url - objectname - Name of the file in the bucket - mb_size - Amount of megabytes to be sent in each iteration - """ - if upload_defer: - if link is None or objectname is None: - raise StorageException( - "Upload-Defer mode requires a link and objectname" - ) - - target_file = objectname if upload_defer else os.path.split(filename)[1] - chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size - size = None - self._filestore.update_file_headers( - target_file, "Content-Type", "application/offset+octet-stream" - ) - storage_link = link if upload_defer else self._filestore.get_link(target_file) - - if upload_defer: - size = str(os.stat(filename).st_size) - self._filestore.update_file_headers(target_file, "Upload-Length", size) - - while True: - headers = self._filestore.get_file_headers(target_file) - offset = self.resumable_offset(storage_link, headers) - file = self._filestore.open_file(filename, offset=int(offset)) - self._filestore.update_file_headers(target_file, "Upload-Offset", offset) - - chunk = file.read(chunk_size) - headers = self._filestore.get_file_headers(target_file) - response = self._client.patch(storage_link, headers=headers, data=chunk) - - if response.status_code not in {201, 204}: - raise StorageException(response.content) - - if "tus-complete" in response.headers: - self._filestore.close_file(file) - self._filestore.remove_file(target_file) - break From 1d6b144c456e5014cc085564b395eae500991fc1 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 9 Sep 2024 18:44:17 -0300 Subject: [PATCH 15/88] feat: More progress --- storage3/_async/resumable.py | 182 +++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 storage3/_async/resumable.py diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py new file mode 100644 index 00000000..c92b659e --- /dev/null +++ b/storage3/_async/resumable.py @@ -0,0 +1,182 @@ +import os +from base64 import b64encode +from datetime import datetime + +from ..types import FileInfo, UploadMetadata +from ..utils import AsyncClient, FileStore, StorageException + + +__all__ = ("AsyncResumableUpload", ) + + +class AsyncResumableUpload: + def __init__(self, session: AsyncClient) -> None: + self._client = session + self.url = f"{self._client.base_url}upload/resumable" + self.expiration_time_format = "%a, %d %b %Y %X %Z" + self._filestore = FileStore() + + def _encode(self, metadata: UploadMetadata) -> str: + """Generate base64 encoding for Upload-Metadata header + Parameters + ---------- + metadata + Bucket and object pair representing the resulting file in the storage + """ + res = [ + f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items() + ] + return ",".join(res) + + def file_exists(self, filename) -> bool: + """Verify if the file exists in the storage + Parameters + ---------- + filename + This could be the local filename or objectname in the storage + """ + return filename in self._filestore.storage + + def get_link(self, objectname) -> str: + """Get the link associated with objectname in the bucket + Parameters + ---------- + objectname + This could be the local filename or objectname in the storage + """ + if not self.file_exists(objectname): + raise StorageException(f"There is no entry for {objectname} in FileStore") + return self._filestore.get_link(objectname) + + async def create_unique_link( + self, bucketname=None, objectname=None, filename=None + ) -> None: + """Create unique link according to bucketname and objectname + Parameters + ---------- + bucketname + Storage bucket + objectname + Filename in the bucket + filename + Local file + """ + if bucketname is None: + raise StorageException("bucketname cannot be empty") + + if objectname is None and filename is None: + raise StorageException("Must specify objectname or filename") + + file = None + upload_mode = None + + if filename: + _, file = os.path.split(filename) + else: + file = objectname + + info = FileInfo( + name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} + ) + + if not filename: + upload_mode = "Upload-Defer-Length" + info["headers"][upload_mode] = "1" + else: + upload_mode = "Upload-Length" + size = str(os.stat(filename).st_size) + + if int(size) == 0: + raise StorageException( + f"Cannot create a link for an empty file: {file}" + ) + + info["headers"][upload_mode] = size + info["length"] = size + + metadata = UploadMetadata(bucketName=bucketname, objectName=file) + + info["headers"]["Upload-Metadata"] = self._encode(metadata) + response = await self._client.post(self.url, headers=info["headers"]) + + if response.status_code != 201: + raise StorageException(response.content) + + expiration_time = datetime.strptime( + response.headers["upload-expires"], self.expiration_time_format + ) + info["expiration_time"] = expiration_time.timestamp() + + info["link"] = response.headers["location"] + del info["headers"][upload_mode] + self._filestore.mark_file(info) + + async def resumable_offset(self, link, headers) -> str: + """Get the current offset to be used + Parameters + ---------- + link + Target url + headers + Metadata headers sent to the server + """ + response = await self._client.head(link, headers=headers) + return response.headers["upload-offset"] + + async def upload( + self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 + ) -> None: + """Send file's content in chunks to the target url + Parameters + ---------- + filename + Local file + upload_defer + Requires link and objectname to be True to retrieve file info in the FileStore + link + Target url + objectname + Name of the file in the bucket + mb_size + Amount of megabytes to be sent in each iteration + """ + if upload_defer: + if link is None or objectname is None: + raise StorageException( + "Upload-Defer mode requires a link and objectname" + ) + + target_file = objectname if upload_defer else os.path.split(filename)[1] + chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size + size = None + self._filestore.update_file_headers( + target_file, "Content-Type", "application/offset+octet-stream" + ) + storage_link = link if upload_defer else self._filestore.get_link(target_file) + + if upload_defer: + size = str(os.stat(filename).st_size) + + if int(size) == 0: + raise StorageException(f"Cannot upload an empty file: {filename}") + + self._filestore.update_file_headers(target_file, "Upload-Length", size) + + while True: + headers = self._filestore.get_file_headers(target_file) + offset = await self.resumable_offset(storage_link, headers) + file = self._filestore.open_file(filename, offset=int(offset)) + self._filestore.update_file_headers(target_file, "Upload-Offset", offset) + + chunk = file.read(chunk_size) + headers = self._filestore.get_file_headers(target_file) + response = await self._client.patch( + storage_link, headers=headers, data=chunk + ) + if response.status_code not in {201, 204}: + raise StorageException(response.content) + + if "tus-complete" in response.headers: + self._filestore.close_file(file) + self._filestore.remove_file(target_file) + break From 594413e145633d2ba11e1da879c5b9986ca6f6aa Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 9 Sep 2024 18:44:24 -0300 Subject: [PATCH 16/88] feat: More progress --- storage3/_sync/resumable.py | 181 ++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 storage3/_sync/resumable.py diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py new file mode 100644 index 00000000..6894c0dd --- /dev/null +++ b/storage3/_sync/resumable.py @@ -0,0 +1,181 @@ +import os +from base64 import b64encode +from datetime import datetime + +from ..types import FileInfo, UploadMetadata +from ..utils import FileStore, StorageException, SyncClient + + +__all__ = ("ResumableUpload",) + + +class ResumableUpload: + def __init__(self, session: SyncClient) -> None: + self._client = session + self.url = f"{self._client.base_url}upload/resumable" + self.expiration_time_format = "%a, %d %b %Y %X %Z" + self._filestore = FileStore() + + def _encode(self, metadata: UploadMetadata) -> str: + """Generate base64 encoding for Upload-Metadata header + Parameters + ---------- + metadata + Bucket and object pair representing the resulting file in the storage + """ + res = [ + f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items() + ] + return ",".join(res) + + def file_exists(self, filename) -> bool: + """Verify if the file exists in the storage + Parameters + ---------- + filename + This could be the local filename or objectname in the storage + """ + return filename in self._filestore.storage + + def get_link(self, objectname) -> str: + """Get the link associated with objectname in the bucket + Parameters + ---------- + objectname + This could be the local filename or objectname in the storage + """ + if not self.file_exists(objectname): + raise StorageException(f"There is no entry for {objectname} in FileStore") + return self._filestore.get_link(objectname) + + def create_unique_link( + self, bucketname=None, objectname=None, filename=None + ) -> None: + """Create unique link according to bucketname and objectname + Parameters + ---------- + bucketname + Storage bucket + objectname + Filename in the bucket + filename + Local file + """ + if bucketname is None: + raise StorageException("bucketname cannot be empty") + + if objectname is None and filename is None: + raise StorageException("Must specify objectname or filename") + + file = None + upload_mode = None + + if filename: + _, file = os.path.split(filename) + else: + file = objectname + + info = FileInfo( + name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} + ) + + if not filename: + upload_mode = "Upload-Defer-Length" + info["headers"][upload_mode] = "1" + else: + upload_mode = "Upload-Length" + size = str(os.stat(filename).st_size) + + if int(size) == 0: + raise StorageException( + f"Cannot create a link for an empty file: {file}" + ) + + info["headers"][upload_mode] = size + info["length"] = size + + metadata = UploadMetadata(bucketName=bucketname, objectName=file) + + info["headers"]["Upload-Metadata"] = self._encode(metadata) + response = self._client.post(self.url, headers=info["headers"]) + + if response.status_code != 201: + raise StorageException(response.content) + + expiration_time = datetime.strptime( + response.headers["upload-expires"], self.expiration_time_format + ) + info["expiration_time"] = expiration_time.timestamp() + + info["link"] = response.headers["location"] + del info["headers"][upload_mode] + self._filestore.mark_file(info) + + def resumable_offset(self, link, headers) -> str: + """Get the current offset to be used + Parameters + ---------- + link + Target url + headers + Metadata headers sent to the server + """ + response = self._client.head(link, headers=headers) + return response.headers["upload-offset"] + + def upload( + self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 + ) -> None: + """Send file's content in chunks to the target url + Parameters + ---------- + filename + Local file + upload_defer + Requires link and objectname to be True to retrieve file info in the FileStore + link + Target url + objectname + Name of the file in the bucket + mb_size + Amount of megabytes to be sent in each iteration + """ + if upload_defer: + if link is None or objectname is None: + raise StorageException( + "Upload-Defer mode requires a link and objectname" + ) + + target_file = objectname if upload_defer else os.path.split(filename)[1] + chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size + size = None + self._filestore.update_file_headers( + target_file, "Content-Type", "application/offset+octet-stream" + ) + storage_link = link if upload_defer else self._filestore.get_link(target_file) + + if upload_defer: + size = str(os.stat(filename).st_size) + + if int(size) == 0: + raise StorageException(f"Cannot upload an empty file: {filename}") + + self._filestore.update_file_headers(target_file, "Upload-Length", size) + + while True: + headers = self._filestore.get_file_headers(target_file) + offset = self.resumable_offset(storage_link, headers) + file = self._filestore.open_file(filename, offset=int(offset)) + self._filestore.update_file_headers(target_file, "Upload-Offset", offset) + + chunk = file.read(chunk_size) + headers = self._filestore.get_file_headers(target_file) + response = self._client.patch(storage_link, headers=headers, data=chunk) + + if response.status_code not in {201, 204}: + raise StorageException(response.content) + + if "tus-complete" in response.headers: + self._filestore.close_file(file) + self._filestore.remove_file(target_file) + break From 49da68e103f641631b4a9e0df9914a94e4e9cece Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 21:53:23 -0300 Subject: [PATCH 17/88] fix: style --- storage3/_async/bucket.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage3/_async/bucket.py b/storage3/_async/bucket.py index 04609c2d..66b141db 100644 --- a/storage3/_async/bucket.py +++ b/storage3/_async/bucket.py @@ -9,8 +9,7 @@ from .file_api import AsyncBucket from .resumable import AsyncResumableUpload - -__all__ = ("AsyncStorageBucketAPI", ) +__all__ = ("AsyncStorageBucketAPI",) class AsyncStorageBucketAPI: From 3725cf0dc2ed36513aa6d70358473fd9dbf32e4e Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 21:53:28 -0300 Subject: [PATCH 18/88] fix: style --- storage3/_async/resumable.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index c92b659e..d59e9b35 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -5,8 +5,7 @@ from ..types import FileInfo, UploadMetadata from ..utils import AsyncClient, FileStore, StorageException - -__all__ = ("AsyncResumableUpload", ) +__all__ = ("AsyncResumableUpload",) class AsyncResumableUpload: From 46af272a86fd490adc0bf831b948175628d02ef4 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 21:53:32 -0300 Subject: [PATCH 19/88] fix: style --- storage3/_sync/bucket.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index cda34b42..e2dcb2f6 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -10,8 +10,7 @@ from .file_api import SyncBucket from .resumable import ResumableUpload - -__all__ = ("SyncStorageBucketAPI", ) +__all__ = ("SyncStorageBucketAPI",) class SyncStorageBucketAPI: From 2c5edd406ad19868d88a198ccdec06b03e1932f0 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 21:53:38 -0300 Subject: [PATCH 20/88] fix: style --- storage3/_sync/resumable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 6894c0dd..319d69de 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -5,7 +5,6 @@ from ..types import FileInfo, UploadMetadata from ..utils import FileStore, StorageException, SyncClient - __all__ = ("ResumableUpload",) From b51d38c9f9fb1c965cad25f0d2d1498ef6023f2b Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 22:01:07 -0300 Subject: [PATCH 21/88] feat: progress --- storage3/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storage3/types.py b/storage3/types.py index 96389227..3d1071c1 100644 --- a/storage3/types.py +++ b/storage3/types.py @@ -93,3 +93,5 @@ class FileInfo(TypedDict): length: str headers: Dict[str, str] expiration_time: float + fingerprint: str + mtime: float From a041bd44c794bf5ed9db3ff2fb9d01bce7048cf9 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 22:29:12 -0300 Subject: [PATCH 22/88] feat: Allow to terminate upload link --- storage3/_async/resumable.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index d59e9b35..39fde7f3 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -122,6 +122,20 @@ async def resumable_offset(self, link, headers) -> str: response = await self._client.head(link, headers=headers) return response.headers["upload-offset"] + async def terminate(self, file: str) -> None: + """Drop the link associated with a file + + Parameters + ---------- + file + file name used to get its metadata info + """ + info = self._filestore.get_file_info(file) + response = await self._client.delete(info["link"], headers=info["headers"]) + + if response.status_code != 204: + raise StorageException(response.content) + async def upload( self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 ) -> None: From 60d5f87d20e4d9d0a071abaa9f5250e7e1784f63 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 22:29:18 -0300 Subject: [PATCH 23/88] feat: Allow to terminate upload link --- storage3/_sync/resumable.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 319d69de..059b511e 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -122,6 +122,20 @@ def resumable_offset(self, link, headers) -> str: response = self._client.head(link, headers=headers) return response.headers["upload-offset"] + def terminate(self, file: str) -> None: + """Drop the link associated with a file + + Parameters + ---------- + file + file name used to get its metadata info + """ + info = self._filestore.get_file_info(file) + response = self._client.delete(info["link"], headers=info["headers"]) + + if response.status_code != 204: + raise StorageException(response.content) + def upload( self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 ) -> None: From f38cc33f2ae59d4c09e2cc0b1003a8b774d5aeda Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 22:46:53 -0300 Subject: [PATCH 24/88] feat: update Fileinfo --- storage3/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/storage3/utils.py b/storage3/utils.py index c1ffb569..ff7dbc85 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -1,3 +1,7 @@ +import json +import os +from hashlib import md5 + from httpx import AsyncClient as AsyncClient # noqa: F401 from httpx import Client as BaseClient @@ -19,9 +23,29 @@ class FileStore: def __init__(self): self.storage = {} + def fingerprint(self, file_info: FileInfo): + """Generates a fingerprint based on the content of the file being sent""" + + block_size = 64 * 1024 + min_size = min(block_size, int(file_info["length"])) + + with open(file_info["name"], "rb") as f: + data = f.read(min_size) + file_info["fingerprint"] = md5(data).hexdigest() + + def persist(self): + with open("resumable_filestore.json", "w") as f: + json.dump(self.storage, f) + def mark_file(self, file_info: FileInfo): """Store file metadata in a in-memory storage""" + + if len(file_info["length"]) != 0: + self.fingerprint(file_info) + file_info["mtime"] = os.stat(file_info["name"]).st_mtime + self.storage[file_info["name"]] = file_info + self.persist() def get_file_info(self, filename): return self.storage[filename] From d934d71d68f64bc7c95302e858dd4d6c618da70e Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 23:34:32 -0300 Subject: [PATCH 25/88] fix: update --- storage3/_sync/resumable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 059b511e..66401b0a 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -136,6 +136,8 @@ def terminate(self, file: str) -> None: if response.status_code != 204: raise StorageException(response.content) + self._filestore.remove_file(file) + def upload( self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 ) -> None: From 197b3f176568beec899f77f1c3a44ddc2b7b0dac Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 23:34:38 -0300 Subject: [PATCH 26/88] fix: update --- storage3/_async/resumable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 39fde7f3..67a7b4e3 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -136,6 +136,8 @@ async def terminate(self, file: str) -> None: if response.status_code != 204: raise StorageException(response.content) + self._filestore.remove_file(file) + async def upload( self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 ) -> None: From bc8d58fec11fdc32ca6a0ac5101263593d07d550 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 23:34:45 -0300 Subject: [PATCH 27/88] fix: update --- storage3/utils.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/storage3/utils.py b/storage3/utils.py index ff7dbc85..de7a588d 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -1,5 +1,6 @@ import json import os +from datetime import datetime from hashlib import md5 from httpx import AsyncClient as AsyncClient # noqa: F401 @@ -22,6 +23,8 @@ class FileStore: def __init__(self): self.storage = {} + self.store_name = "resumable_filestore.json" + self.reload_storage() def fingerprint(self, file_info: FileInfo): """Generates a fingerprint based on the content of the file being sent""" @@ -34,8 +37,8 @@ def fingerprint(self, file_info: FileInfo): file_info["fingerprint"] = md5(data).hexdigest() def persist(self): - with open("resumable_filestore.json", "w") as f: - json.dump(self.storage, f) + with open(self.store_name, "w") as f: + json.dump(self.storage, f, indent=2) def mark_file(self, file_info: FileInfo): """Store file metadata in a in-memory storage""" @@ -48,17 +51,30 @@ def mark_file(self, file_info: FileInfo): self.persist() def get_file_info(self, filename): + self.reload_storage() return self.storage[filename] + def reload_storage(self): + with open(self.store_name) as f: + self.storage = json.load(f) + def update_file_headers(self, filename, key, value): file = self.get_file_info(filename) - file["headers"][key] = value + is_link_expired = file["expiration_time"] < datetime.now().timestamp() + + if not is_link_expired: + file["headers"][key] = value + self.storage[filename] = file + self.persist() + else: + self.remove_file(filename) + raise StorageException("Upload link is expired") def get_file_headers(self, filename): return self.get_file_info(filename)["headers"] def get_file_storage_link(self, filename): - return self.get_file_info(filename)["headers"]["link"] + return self.get_file_headers(filename)["link"] def open_file(self, filename: str, offset: int): """Open file in the specified offset @@ -78,6 +94,7 @@ def close_file(self, filename): def remove_file(self, filename: str): del self.storage[filename] + self.persist() def get_link(self, filename: str): return self.storage[filename]["link"] From b659780fb658ff6b53a3520b0c4aededea30d30a Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 23:39:55 -0300 Subject: [PATCH 28/88] fix: update --- storage3/_sync/bucket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index e2dcb2f6..e96ba225 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -4,12 +4,12 @@ from httpx import HTTPError, Response -from ..resumable import ResumableUpload from ..types import CreateOrUpdateBucketOptions, RequestMethod from ..utils import StorageException, SyncClient from .file_api import SyncBucket from .resumable import ResumableUpload + __all__ = ("SyncStorageBucketAPI",) From 01cfb62ad02e0fecf394ffa756d07b8f7644c56c Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 10 Sep 2024 23:44:20 -0300 Subject: [PATCH 29/88] fix: style --- storage3/_sync/bucket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index e96ba225..efd9c8a4 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -9,7 +9,6 @@ from .file_api import SyncBucket from .resumable import ResumableUpload - __all__ = ("SyncStorageBucketAPI",) From 8da95d215f1b6bb58d7f6456fe31e96d13d4921a Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 16 Sep 2024 19:18:16 -0300 Subject: [PATCH 30/88] fix: Deferred upload working wip --- storage3/utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/storage3/utils.py b/storage3/utils.py index de7a588d..f175acbc 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -70,6 +70,13 @@ def update_file_headers(self, filename, key, value): self.remove_file(filename) raise StorageException("Upload link is expired") + def delete_file_headers(self, filename, key): + file = self.get_file_info(filename) + if key in file["headers"]: + del file["headers"][key] + self.storage[filename] = file + self.persist() + def get_file_headers(self, filename): return self.get_file_info(filename)["headers"] From 0568eda90ca5acb12cae10c01136424cb5b6b0a0 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 16 Sep 2024 19:23:51 -0300 Subject: [PATCH 31/88] fix: Deferred upload working wip --- storage3/_sync/resumable.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 66401b0a..081414e6 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -66,14 +66,9 @@ def create_unique_link( if objectname is None and filename is None: raise StorageException("Must specify objectname or filename") - file = None + file = filename if filename else objectname upload_mode = None - if filename: - _, file = os.path.split(filename) - else: - file = objectname - info = FileInfo( name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} ) @@ -93,7 +88,8 @@ def create_unique_link( info["headers"][upload_mode] = size info["length"] = size - metadata = UploadMetadata(bucketName=bucketname, objectName=file) + obj_name = os.path.split(file)[1] + metadata = UploadMetadata(bucketName=bucketname, objectName=obj_name) info["headers"]["Upload-Metadata"] = self._encode(metadata) response = self._client.post(self.url, headers=info["headers"]) @@ -161,7 +157,7 @@ def upload( "Upload-Defer mode requires a link and objectname" ) - target_file = objectname if upload_defer else os.path.split(filename)[1] + target_file = objectname if upload_defer else filename chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( @@ -176,6 +172,10 @@ def upload( raise StorageException(f"Cannot upload an empty file: {filename}") self._filestore.update_file_headers(target_file, "Upload-Length", size) + self._filestore.update_file_headers(target_file, "Upload-Offset", "0") + headers = self._filestore.get_file_headers(target_file) + response = self._client.patch(storage_link, headers=headers) + self._filestore.delete_file_headers(target_file, "Upload-Length") while True: headers = self._filestore.get_file_headers(target_file) From 95d577ffa1e6242442bfacb4fe8abdbf548725fb Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 16 Sep 2024 19:26:36 -0300 Subject: [PATCH 32/88] fix: Deferred upload working wip --- storage3/_async/resumable.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 67a7b4e3..cc66adc8 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -66,14 +66,9 @@ async def create_unique_link( if objectname is None and filename is None: raise StorageException("Must specify objectname or filename") - file = None + file = filename if filename else objectname upload_mode = None - if filename: - _, file = os.path.split(filename) - else: - file = objectname - info = FileInfo( name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} ) @@ -93,7 +88,8 @@ async def create_unique_link( info["headers"][upload_mode] = size info["length"] = size - metadata = UploadMetadata(bucketName=bucketname, objectName=file) + obj_name = os.path.split(file)[1] + metadata = UploadMetadata(bucketName=bucketname, objectName=obj_name) info["headers"]["Upload-Metadata"] = self._encode(metadata) response = await self._client.post(self.url, headers=info["headers"]) @@ -161,7 +157,7 @@ async def upload( "Upload-Defer mode requires a link and objectname" ) - target_file = objectname if upload_defer else os.path.split(filename)[1] + target_file = objectname if upload_defer else filename chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( @@ -176,6 +172,10 @@ async def upload( raise StorageException(f"Cannot upload an empty file: {filename}") self._filestore.update_file_headers(target_file, "Upload-Length", size) + self._filestore.update_file_headers(target_file, "Upload-Offset", "0") + headers = self._filestore.get_file_headers(target_file) + response = await self._client.patch(storage_link, headers=headers) + self._filestore.delete_file_headers(target_file, "Upload-Length") while True: headers = self._filestore.get_file_headers(target_file) From aab78da35f07a78c0182bc09ce28ab494febc73d Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 17 Sep 2024 18:31:49 -0300 Subject: [PATCH 33/88] fix: Progress --- storage3/utils.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/storage3/utils.py b/storage3/utils.py index f175acbc..89f5bb26 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -1,5 +1,7 @@ import json import os +import tempfile + from datetime import datetime from hashlib import md5 @@ -23,7 +25,7 @@ class FileStore: def __init__(self): self.storage = {} - self.store_name = "resumable_filestore.json" + self.disk_storage = tempfile.NamedTemporaryFile(mode='w+t', delete=False) self.reload_storage() def fingerprint(self, file_info: FileInfo): @@ -37,8 +39,10 @@ def fingerprint(self, file_info: FileInfo): file_info["fingerprint"] = md5(data).hexdigest() def persist(self): - with open(self.store_name, "w") as f: - json.dump(self.storage, f, indent=2) + with open(self.disk_storage.name, 'w') as f: + f.seek(0) + f.write(json.dumps(self.storage)) + f.flush() def mark_file(self, file_info: FileInfo): """Store file metadata in a in-memory storage""" @@ -55,8 +59,11 @@ def get_file_info(self, filename): return self.storage[filename] def reload_storage(self): - with open(self.store_name) as f: - self.storage = json.load(f) + self.storage = {} + size = os.stat(self.disk_storage.name).st_size + if size > 0: + with open(self.disk_storage.name) as f: + self.storage = json.load(f) def update_file_headers(self, filename, key, value): file = self.get_file_info(filename) @@ -80,9 +87,6 @@ def delete_file_headers(self, filename, key): def get_file_headers(self, filename): return self.get_file_info(filename)["headers"] - def get_file_storage_link(self, filename): - return self.get_file_headers(filename)["link"] - def open_file(self, filename: str, offset: int): """Open file in the specified offset Parameters From 13513bcad6b90bfc485e1f42e35055d32dff269e Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 17 Sep 2024 18:35:08 -0300 Subject: [PATCH 34/88] fix: Progress --- storage3/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/storage3/utils.py b/storage3/utils.py index 89f5bb26..85c8cafc 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -1,7 +1,6 @@ import json import os import tempfile - from datetime import datetime from hashlib import md5 @@ -25,7 +24,7 @@ class FileStore: def __init__(self): self.storage = {} - self.disk_storage = tempfile.NamedTemporaryFile(mode='w+t', delete=False) + self.disk_storage = tempfile.NamedTemporaryFile(mode="w+t", delete=False) self.reload_storage() def fingerprint(self, file_info: FileInfo): @@ -39,7 +38,7 @@ def fingerprint(self, file_info: FileInfo): file_info["fingerprint"] = md5(data).hexdigest() def persist(self): - with open(self.disk_storage.name, 'w') as f: + with open(self.disk_storage.name, "w") as f: f.seek(0) f.write(json.dumps(self.storage)) f.flush() From 3c8ed17197849b2724caa695d3b2852623301f74 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 17 Sep 2024 18:54:41 -0300 Subject: [PATCH 35/88] fix: Progress --- storage3/utils.py | 79 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/storage3/utils.py b/storage3/utils.py index 85c8cafc..ecb2d26b 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -3,6 +3,7 @@ import tempfile from datetime import datetime from hashlib import md5 +from typing import Dict from httpx import AsyncClient as AsyncClient # noqa: F401 from httpx import Client as BaseClient @@ -23,7 +24,6 @@ class FileStore: """This class serves as storage of files to be sent in the resumable upload workflow""" def __init__(self): - self.storage = {} self.disk_storage = tempfile.NamedTemporaryFile(mode="w+t", delete=False) self.reload_storage() @@ -37,7 +37,8 @@ def fingerprint(self, file_info: FileInfo): data = f.read(min_size) file_info["fingerprint"] = md5(data).hexdigest() - def persist(self): + def persist(self) -> None: + """Save the current state of in-memory storage to disk""" with open(self.disk_storage.name, "w") as f: f.seek(0) f.write(json.dumps(self.storage)) @@ -53,18 +54,37 @@ def mark_file(self, file_info: FileInfo): self.storage[file_info["name"]] = file_info self.persist() - def get_file_info(self, filename): - self.reload_storage() - return self.storage[filename] - - def reload_storage(self): + def reload_storage(self) -> None: + """Refresh the in-memory storage""" self.storage = {} size = os.stat(self.disk_storage.name).st_size if size > 0: with open(self.disk_storage.name) as f: self.storage = json.load(f) - def update_file_headers(self, filename, key, value): + def get_file_info(self, filename) -> FileInfo: + """Returns the file info metadata associated with a filename in the storage + + Parameters + ---------- + filename + key name referencing to filename attributes. + """ + self.reload_storage() + return self.storage[filename] + + def update_file_headers(self, filename, key, value) -> None: + """Update key values from the file info metadata + + Parameters + ---------- + filename + key name referencing to filename attributes. + key + key name referencing to header attribute to be modified + value + new value + """ file = self.get_file_info(filename) is_link_expired = file["expiration_time"] < datetime.now().timestamp() @@ -76,18 +96,35 @@ def update_file_headers(self, filename, key, value): self.remove_file(filename) raise StorageException("Upload link is expired") - def delete_file_headers(self, filename, key): + def delete_file_headers(self, filename, key) -> None: + """Remove keys from the file info metadata + + Parameters + ---------- + filename + key name referencing to filename attributes. + key + key name referencing to header attribute to be removed + """ file = self.get_file_info(filename) if key in file["headers"]: del file["headers"][key] self.storage[filename] = file self.persist() - def get_file_headers(self, filename): + def get_file_headers(self, filename) -> Dict[str, str]: + """Returns the file's headers used during the upload workflow + + Parameters + ---------- + filename + key name referencing to filename attributes. + """ return self.get_file_info(filename)["headers"] def open_file(self, filename: str, offset: int): """Open file in the specified offset + Parameters ---------- filename @@ -96,15 +133,29 @@ def open_file(self, filename: str, offset: int): set current the file-pointer """ file = open(filename, "rb") - file.seek(int(offset)) + file.seek(offset) return file - def close_file(self, filename): + def close_file(self, filename) -> None: filename.close() - def remove_file(self, filename: str): + def remove_file(self, filename: str) -> None: + """Remove filename entry in the in-memory storage and then commit the changes into the disk storage + + Parameters + ---------- + filename + key name referencing to filename attributes. + """ del self.storage[filename] self.persist() - def get_link(self, filename: str): + def get_link(self, filename: str) -> str: + """Returns the filename's link associated with is resumable endpoint + + Parameters + ---------- + filename + key name referencing to filename attributes. + """ return self.storage[filename]["link"] From 761811e857ee77573a9949fbfe0df80f19df6557 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 17 Sep 2024 19:00:10 -0300 Subject: [PATCH 36/88] fix: Progress --- storage3/_sync/bucket.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/storage3/_sync/bucket.py b/storage3/_sync/bucket.py index efd9c8a4..46b976fd 100644 --- a/storage3/_sync/bucket.py +++ b/storage3/_sync/bucket.py @@ -19,13 +19,6 @@ def __init__(self, session: SyncClient) -> None: self._client = session self._resumable = None - @property - def resumable(self): - if self._resumable is None: - self._resumable = ResumableUpload(self._client) - - return self._resumable - def _request( self, method: RequestMethod, @@ -42,6 +35,13 @@ def _request( return response + @property + def resumable(self): + if self._resumable is None: + self._resumable = ResumableUpload(self._client) + + return self._resumable + def list_buckets(self) -> list[SyncBucket]: """Retrieves the details of all storage buckets within an existing product.""" # if the request doesn't error, it is assured to return a list From dc6f3236bced8c25f7d022bb84d2b0ec5cdff74e Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Thu, 19 Sep 2024 10:30:54 -0300 Subject: [PATCH 37/88] fix: arg checking, no empty filenames --- storage3/_async/resumable.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index cc66adc8..655e33b6 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -15,6 +15,9 @@ def __init__(self, session: AsyncClient) -> None: self.expiration_time_format = "%a, %d %b %Y %X %Z" self._filestore = FileStore() + def _is_valid_arg(self, target): + return isinstance(target, str) and len(target.strip()) > 0 + def _encode(self, metadata: UploadMetadata) -> str: """Generate base64 encoding for Upload-Metadata header Parameters @@ -60,15 +63,17 @@ async def create_unique_link( filename Local file """ - if bucketname is None: - raise StorageException("bucketname cannot be empty") + if not self._is_valid_arg(bucketname): + raise StorageException("Bucketname cannot be empty") - if objectname is None and filename is None: + if not (self._is_valid_arg(objectname) or self._is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") file = filename if filename else objectname - upload_mode = None + if not self._is_valid_arg(file): + raise StorageException("Must specify objectname or filename") + upload_mode = None info = FileInfo( name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} ) @@ -152,11 +157,14 @@ async def upload( Amount of megabytes to be sent in each iteration """ if upload_defer: - if link is None or objectname is None: + if not (self._is_valid_arg(link) and self._is_valid_arg(objectname)): raise StorageException( "Upload-Defer mode requires a link and objectname" ) + if not self._is_valid_arg(filename): + raise StorageException("Must specify a filename") + target_file = objectname if upload_defer else filename chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None From bd174d7318eea1e09b9f2c76d0a3f1f393c464d2 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Thu, 19 Sep 2024 10:31:01 -0300 Subject: [PATCH 38/88] fix: arg checking, no empty filenames --- storage3/_sync/resumable.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 081414e6..153dfdd2 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -15,6 +15,9 @@ def __init__(self, session: SyncClient) -> None: self.expiration_time_format = "%a, %d %b %Y %X %Z" self._filestore = FileStore() + def _is_valid_arg(self, target): + return isinstance(target, str) and len(target.strip()) > 0 + def _encode(self, metadata: UploadMetadata) -> str: """Generate base64 encoding for Upload-Metadata header Parameters @@ -60,15 +63,17 @@ def create_unique_link( filename Local file """ - if bucketname is None: + if not self._is_valid_arg(bucketname): raise StorageException("bucketname cannot be empty") - if objectname is None and filename is None: + if not (self._is_valid_arg(objectname) or self._is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") file = filename if filename else objectname - upload_mode = None + if not self._is_valid_arg(file): + raise StorageException("Must specify objectname or filename") + upload_mode = None info = FileInfo( name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} ) @@ -152,11 +157,14 @@ def upload( Amount of megabytes to be sent in each iteration """ if upload_defer: - if link is None or objectname is None: + if not (self._is_valid_arg(link) and self._is_valid_arg(objectname)): raise StorageException( "Upload-Defer mode requires a link and objectname" ) + if not self._is_valid_arg(filename): + raise StorageException("Must specify a filename") + target_file = objectname if upload_defer else filename chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None From 9f8d85bd2b91fcb5a3f10e4bd6536cd423d5407a Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:24:20 -0300 Subject: [PATCH 39/88] fix: improve readability --- storage3/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/utils.py b/storage3/utils.py index ecb2d26b..a6285df0 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -41,7 +41,7 @@ def persist(self) -> None: """Save the current state of in-memory storage to disk""" with open(self.disk_storage.name, "w") as f: f.seek(0) - f.write(json.dumps(self.storage)) + f.write(json.dumps(self.storage, indent=2)) f.flush() def mark_file(self, file_info: FileInfo): From e13466b78b586b53a249001e4619f51792a69588 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:38:10 -0300 Subject: [PATCH 40/88] fix: test resumable WIP --- tests/resumable/test_resumable.py | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/resumable/test_resumable.py diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py new file mode 100644 index 00000000..d8c81c77 --- /dev/null +++ b/tests/resumable/test_resumable.py @@ -0,0 +1,56 @@ +import os + + +def test_sync_client(sync_client, file, test_bucket): + client = sync_client + + """Check file was created during configuration phase""" + assert file is not None + + """Verify test_bucket is not an empty string""" + assert len(test_bucket.strip()) > 0 + + client.resumable.create_unique_link(bucketname=test_bucket, filename=file.name) + link = client.resumable.get_link(file.name) + + """Verify the link was generated as expected""" + assert len(link) > 0 + + """Check the file is not empty""" + assert os.stat(file.name).st_size > 0 + + """Verify if the file was loaded correctly""" + client.resumable.upload(file.name) + bucket = client.get_bucket(test_bucket) + file_loaded = list(filter(lambda e: e["name"] == file.name, bucket.list())) + assert len(file_loaded) == 1 + + bucket.remove(file.name) + + +def test_deferred_sync_client(sync_client, file, test_bucket): + + client = sync_client + + """Check file was created during configuration phase""" + assert file is not None + + """Verify test_bucket is not an empty string""" + assert len(test_bucket.strip()) > 0 + + client.resumable.create_unique_link(bucketname=test_bucket, objectname=file.name) + link = client.resumable.get_link(file.name) + + """Verify the link was generated as expected""" + assert len(link) > 0 + + """Check the file is not empty""" + assert os.stat(file.name).st_size > 0 + + """Verify if the file was loaded correctly""" + client.resumable.upload(file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name) + bucket = client.get_bucket(test_bucket) + file_loaded = list(filter(lambda e: e["name"] == file.name, bucket.list())) + assert len(file_loaded) == 1 + + bucket.remove(file.name) From 9aef96d3d5c985652f0a3981ab0a8fb2169feab6 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:38:43 -0300 Subject: [PATCH 41/88] fix: test resumable WIP --- tests/resumable/test_resumable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index d8c81c77..1a15694b 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -48,7 +48,9 @@ def test_deferred_sync_client(sync_client, file, test_bucket): assert os.stat(file.name).st_size > 0 """Verify if the file was loaded correctly""" - client.resumable.upload(file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name) + client.resumable.upload( + file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name + ) bucket = client.get_bucket(test_bucket) file_loaded = list(filter(lambda e: e["name"] == file.name, bucket.list())) assert len(file_loaded) == 1 From 8fb7841de5822df48d6402683e6929eec6377233 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:43:39 -0300 Subject: [PATCH 42/88] fix: test resumable WIP --- tests/resumable/conftest.py | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/resumable/conftest.py diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py new file mode 100644 index 00000000..5b11d13b --- /dev/null +++ b/tests/resumable/conftest.py @@ -0,0 +1,54 @@ +from __future__ import annotations +import os +import pytest + +from storage3 import SyncStorageClient, AsyncStorageClient + + +@pytest.fixture +def file() -> str: + file_name = "test_image.svg" + file_content = ( + b' ' + b' ' + b' ' + b' ' + b' ' + ) + + with open(file_name, "wb") as f: + f.write(file_content) + + return f + + +@pytest.fixture +def test_bucket() -> str: + return os.getenv("TEST_BUCKET") + + +@pytest.fixture +def configure_client(): + url = f'{os.getenv("SUPABASE_URL")}/storage/v1' + key = os.getenv("SUPABASE_KEY") + return (url, key) + + +@pytest.fixture +def sync_client(configure_client) -> SyncStorageClient: + url, key = configure_client + client = SyncStorageClient(url, {'apiKey': key, 'Authorization': f'Bearer {key}'}) + return client + + +@pytest.fixture +def async_client(configure_client) -> AsyncStorageClient: + url, key = configure_client + client = AsyncStorageClient(url, {'apiKey': key, 'Authorization': f'Bearer {key}'}) + return client From d2fd3244c37179e13fe3dc2e8b52dd231d2a2de6 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:44:22 -0300 Subject: [PATCH 43/88] fix: test resumable WIP --- tests/resumable/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 5b11d13b..69fb8d85 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -1,8 +1,10 @@ from __future__ import annotations + import os + import pytest -from storage3 import SyncStorageClient, AsyncStorageClient +from storage3 import AsyncStorageClient, SyncStorageClient @pytest.fixture @@ -43,12 +45,12 @@ def configure_client(): @pytest.fixture def sync_client(configure_client) -> SyncStorageClient: url, key = configure_client - client = SyncStorageClient(url, {'apiKey': key, 'Authorization': f'Bearer {key}'}) + client = SyncStorageClient(url, {"apiKey": key, "Authorization": f"Bearer {key}"}) return client @pytest.fixture def async_client(configure_client) -> AsyncStorageClient: url, key = configure_client - client = AsyncStorageClient(url, {'apiKey': key, 'Authorization': f'Bearer {key}'}) + client = AsyncStorageClient(url, {"apiKey": key, "Authorization": f"Bearer {key}"}) return client From c5c37ba6c2e67672f09663bf6b217e8195f409bd Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:55:14 -0300 Subject: [PATCH 44/88] fix: test resumable WIP --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index eee398c6..75582ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ sphinx-toolbox = "^3.4.0" [tool.pytest.ini_options] asyncio_mode = "auto" +addopts = --ignore=tests/resumable [build-system] build-backend = "poetry.core.masonry.api" From 64f0fee58961b17eb0b47dc79fe542fe2cb83f8b Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 22:57:55 -0300 Subject: [PATCH 45/88] fix: test resumable WIP --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 75582ec3..e6fc97b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ sphinx-toolbox = "^3.4.0" [tool.pytest.ini_options] asyncio_mode = "auto" -addopts = --ignore=tests/resumable +addopts = "--ignore=tests/resumable" [build-system] build-backend = "poetry.core.masonry.api" From ecc020b49e20690855437c0621e428f8e5a2ebc3 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:04:26 -0300 Subject: [PATCH 46/88] fix: test resumable WIP --- tests/resumable/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 69fb8d85..aab7acb3 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -35,21 +35,21 @@ def test_bucket() -> str: return os.getenv("TEST_BUCKET") -@pytest.fixture +@pytest.fixture(scope="module") def configure_client(): url = f'{os.getenv("SUPABASE_URL")}/storage/v1' key = os.getenv("SUPABASE_KEY") return (url, key) -@pytest.fixture +@pytest.fixture(scope="module") def sync_client(configure_client) -> SyncStorageClient: url, key = configure_client client = SyncStorageClient(url, {"apiKey": key, "Authorization": f"Bearer {key}"}) return client -@pytest.fixture +@pytest.fixture(scope="module") def async_client(configure_client) -> AsyncStorageClient: url, key = configure_client client = AsyncStorageClient(url, {"apiKey": key, "Authorization": f"Bearer {key}"}) From 16987df519ceff5a25cf121cd9edfe9f8f3d64a8 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:16:36 -0300 Subject: [PATCH 47/88] fix: Use content instead of data --- storage3/_async/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 655e33b6..0188ceab 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -194,7 +194,7 @@ async def upload( chunk = file.read(chunk_size) headers = self._filestore.get_file_headers(target_file) response = await self._client.patch( - storage_link, headers=headers, data=chunk + storage_link, headers=headers, content=chunk ) if response.status_code not in {201, 204}: raise StorageException(response.content) From 4fef9af147d3957b72860ee85528a81b73f92581 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:16:42 -0300 Subject: [PATCH 48/88] fix: Use content instead of data --- storage3/_sync/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 153dfdd2..a2a74376 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -193,7 +193,7 @@ def upload( chunk = file.read(chunk_size) headers = self._filestore.get_file_headers(target_file) - response = self._client.patch(storage_link, headers=headers, data=chunk) + response = self._client.patch(storage_link, headers=headers, content=chunk) if response.status_code not in {201, 204}: raise StorageException(response.content) From 927f1a2703e821f8234e0afd6d04d0133825e3df Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:36:18 -0300 Subject: [PATCH 49/88] fix: test resumable WIP --- tests/resumable/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index aab7acb3..5dbc58f9 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -1,8 +1,10 @@ from __future__ import annotations import os +import asyncio import pytest +import pytest_asyncio from storage3 import AsyncStorageClient, SyncStorageClient @@ -35,6 +37,12 @@ def test_bucket() -> str: return os.getenv("TEST_BUCKET") +@pytest_asyncio.fixture(scope="package") +def event_loop() -> asyncio.AbstractEventLoop: + """Returns an event loop for the current thread""" + return asyncio.get_event_loop_policy().get_event_loop() + + @pytest.fixture(scope="module") def configure_client(): url = f'{os.getenv("SUPABASE_URL")}/storage/v1' From 6d37bb82736085b5b938a64b36cc4db36cf24e9c Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:36:29 -0300 Subject: [PATCH 50/88] fix: test resumable WIP --- tests/resumable/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 5dbc58f9..50819515 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -1,7 +1,7 @@ from __future__ import annotations -import os import asyncio +import os import pytest import pytest_asyncio From 200e399311e9e32594414ebd5174692f87645710 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:37:47 -0300 Subject: [PATCH 51/88] fix: test resumable WIP --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e6fc97b9..dbc44a6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ sphinx-toolbox = "^3.4.0" [tool.pytest.ini_options] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" addopts = "--ignore=tests/resumable" [build-system] From 92b246470b2be6dd789476916ea46ab6763b87d5 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:47:57 -0300 Subject: [PATCH 52/88] fix: test resumable async WIP --- tests/resumable/test_resumable.py | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index 1a15694b..1a5328ec 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -56,3 +56,64 @@ def test_deferred_sync_client(sync_client, file, test_bucket): assert len(file_loaded) == 1 bucket.remove(file.name) + + +async def test_async_client(async_client, file, test_bucket): + client = async_client + + """Check file was created during configuration phase""" + assert file is not None + + """Verify test_bucket is not an empty string""" + assert len(test_bucket.strip()) > 0 + + await client.resumable.create_unique_link( + bucketname=test_bucket, filename=file.name + ) + link = client.resumable.get_link(file.name) + + """Verify the link was generated as expected""" + assert len(link) > 0 + + """Check the file is not empty""" + assert os.stat(file.name).st_size > 0 + + """Verify if the file was loaded correctly""" + await client.resumable.upload(file.name) + bucket = await client.get_bucket(test_bucket) + file_loaded = list(filter(lambda e: e["name"] == file.name, await bucket.list())) + assert len(file_loaded) == 1 + + await bucket.remove(file.name) + + +async def test_deferred_async_client(async_client, file, test_bucket): + + client = async_client + + """Check file was created during configuration phase""" + assert file is not None + + """Verify test_bucket is not an empty string""" + assert len(test_bucket.strip()) > 0 + + await client.resumable.create_unique_link( + bucketname=test_bucket, objectname=file.name + ) + link = client.resumable.get_link(file.name) + + """Verify the link was generated as expected""" + assert len(link) > 0 + + """Check the file is not empty""" + assert os.stat(file.name).st_size > 0 + + """Verify if the file was loaded correctly""" + await client.resumable.upload( + file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name + ) + bucket = await client.get_bucket(test_bucket) + file_loaded = list(filter(lambda e: e["name"] == file.name, await bucket.list())) + assert len(file_loaded) == 1 + + await bucket.remove(file.name) From a4403361b560b29c3f5c96af89c175e50e975082 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 14 Oct 2024 23:49:41 -0300 Subject: [PATCH 53/88] fix: test resumable async WIP --- tests/resumable/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 50819515..6aa11601 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -32,17 +32,17 @@ def file() -> str: return f -@pytest.fixture -def test_bucket() -> str: - return os.getenv("TEST_BUCKET") - - @pytest_asyncio.fixture(scope="package") def event_loop() -> asyncio.AbstractEventLoop: """Returns an event loop for the current thread""" return asyncio.get_event_loop_policy().get_event_loop() +@pytest.fixture +def test_bucket() -> str: + return os.getenv("TEST_BUCKET") + + @pytest.fixture(scope="module") def configure_client(): url = f'{os.getenv("SUPABASE_URL")}/storage/v1' From 992f438aa8ae4f703cb7348484d7523b6b0af821 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 15 Oct 2024 21:42:11 -0300 Subject: [PATCH 54/88] fix: verify the link was generated as expected --- tests/resumable/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 6aa11601..7e3693d4 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -2,6 +2,7 @@ import asyncio import os +from urllib.parse import urlparse import pytest import pytest_asyncio @@ -9,6 +10,11 @@ from storage3 import AsyncStorageClient, SyncStorageClient +def is_https_url(url: str) -> bool: + """Simple helper for testing purposes.""" + return urlparse(url).scheme == "https" + + @pytest.fixture def file() -> str: file_name = "test_image.svg" From f16ca11cbe7cbab51aaf973ea4f5a446753190ba Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 15 Oct 2024 21:42:16 -0300 Subject: [PATCH 55/88] fix: verify the link was generated as expected --- tests/resumable/test_resumable.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index 1a5328ec..41facad6 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -1,5 +1,7 @@ import os +from conftest import is_https_url + def test_sync_client(sync_client, file, test_bucket): client = sync_client @@ -14,7 +16,7 @@ def test_sync_client(sync_client, file, test_bucket): link = client.resumable.get_link(file.name) """Verify the link was generated as expected""" - assert len(link) > 0 + assert is_https_url(link) """Check the file is not empty""" assert os.stat(file.name).st_size > 0 @@ -42,7 +44,7 @@ def test_deferred_sync_client(sync_client, file, test_bucket): link = client.resumable.get_link(file.name) """Verify the link was generated as expected""" - assert len(link) > 0 + assert is_https_url(link) """Check the file is not empty""" assert os.stat(file.name).st_size > 0 @@ -73,7 +75,7 @@ async def test_async_client(async_client, file, test_bucket): link = client.resumable.get_link(file.name) """Verify the link was generated as expected""" - assert len(link) > 0 + assert is_https_url(link) """Check the file is not empty""" assert os.stat(file.name).st_size > 0 @@ -103,7 +105,7 @@ async def test_deferred_async_client(async_client, file, test_bucket): link = client.resumable.get_link(file.name) """Verify the link was generated as expected""" - assert len(link) > 0 + assert is_https_url(link) """Check the file is not empty""" assert os.stat(file.name).st_size > 0 From 0ba74853d33cdba7aeb4badd91719bd6b1a1b2ff Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 15 Oct 2024 21:45:22 -0300 Subject: [PATCH 56/88] fix: style, improve readability --- tests/resumable/test_resumable.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index 41facad6..e99a77b1 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -24,8 +24,9 @@ def test_sync_client(sync_client, file, test_bucket): """Verify if the file was loaded correctly""" client.resumable.upload(file.name) bucket = client.get_bucket(test_bucket) - file_loaded = list(filter(lambda e: e["name"] == file.name, bucket.list())) - assert len(file_loaded) == 1 + + is_file_loaded = any(item["name"] == file.name for item in bucket.list()) + assert is_file_loaded, f"File not loaded:\n{bucket.list()}" bucket.remove(file.name) @@ -54,8 +55,9 @@ def test_deferred_sync_client(sync_client, file, test_bucket): file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name ) bucket = client.get_bucket(test_bucket) - file_loaded = list(filter(lambda e: e["name"] == file.name, bucket.list())) - assert len(file_loaded) == 1 + + is_file_loaded = any(item["name"] == file.name for item in bucket.list()) + assert is_file_loaded, f"File not loaded:\n{bucket.list()}" bucket.remove(file.name) @@ -83,8 +85,9 @@ async def test_async_client(async_client, file, test_bucket): """Verify if the file was loaded correctly""" await client.resumable.upload(file.name) bucket = await client.get_bucket(test_bucket) - file_loaded = list(filter(lambda e: e["name"] == file.name, await bucket.list())) - assert len(file_loaded) == 1 + + is_file_loaded = any(item["name"] == file.name for item in await bucket.list()) + assert is_file_loaded, f"File not loaded:\n{bucket.list()}" await bucket.remove(file.name) @@ -115,7 +118,8 @@ async def test_deferred_async_client(async_client, file, test_bucket): file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name ) bucket = await client.get_bucket(test_bucket) - file_loaded = list(filter(lambda e: e["name"] == file.name, await bucket.list())) - assert len(file_loaded) == 1 + + is_file_loaded = any(item["name"] == file.name for item in await bucket.list()) + assert is_file_loaded, f"File not loaded:\n{bucket.list()}" await bucket.remove(file.name) From 7b868591ae912de5ce7571a664201058c8863d51 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:44:04 -0300 Subject: [PATCH 57/88] fix: Add more tests --- storage3/utils.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/storage3/utils.py b/storage3/utils.py index a6285df0..bfbadd0c 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -62,6 +62,17 @@ def reload_storage(self) -> None: with open(self.disk_storage.name) as f: self.storage = json.load(f) + def file_exists(self, filename: str) -> bool: + """Verify if the file exists in the storage + + Parameters + ---------- + filename + This could be the local filename or objectname in the storage + """ + self.reload_storage() + return filename in self.storage + def get_file_info(self, filename) -> FileInfo: """Returns the file info metadata associated with a filename in the storage @@ -70,7 +81,9 @@ def get_file_info(self, filename) -> FileInfo: filename key name referencing to filename attributes. """ - self.reload_storage() + if not self.file_exists(filename): + raise StorageException(f"There is no entry for {filename} in FileStore") + return self.storage[filename] def update_file_headers(self, filename, key, value) -> None: @@ -147,15 +160,33 @@ def remove_file(self, filename: str) -> None: filename key name referencing to filename attributes. """ + + if not self.file_exists(filename): + raise StorageException(f"There is no entry for {filename} in FileStore") + del self.storage[filename] self.persist() def get_link(self, filename: str) -> str: - """Returns the filename's link associated with is resumable endpoint + """Returns the filename's link associated with its resumable endpoint Parameters ---------- filename key name referencing to filename attributes. """ + + if not self.file_exists(filename): + raise StorageException(f"There is no entry for {filename} in FileStore") + return self.storage[filename]["link"] + + def link_exists(self, link: str) -> bool: + """Check if the link is already in the storage + + Parameters + ---------- + link: + link associated with a resumable endpoint + """ + return any(self.get_link(obj) == link for obj in self.storage.keys()) From 9b9926af324cd73a0bd0a1a12d0054809412d2fd Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:44:09 -0300 Subject: [PATCH 58/88] fix: Add more tests --- storage3/_async/resumable.py | 57 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 0188ceab..87754cd7 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -5,7 +5,7 @@ from ..types import FileInfo, UploadMetadata from ..utils import AsyncClient, FileStore, StorageException -__all__ = ("AsyncResumableUpload",) +__all__ = ["AsyncResumableUpload"] class AsyncResumableUpload: @@ -15,11 +15,9 @@ def __init__(self, session: AsyncClient) -> None: self.expiration_time_format = "%a, %d %b %Y %X %Z" self._filestore = FileStore() - def _is_valid_arg(self, target): - return isinstance(target, str) and len(target.strip()) > 0 - def _encode(self, metadata: UploadMetadata) -> str: """Generate base64 encoding for Upload-Metadata header + Parameters ---------- metadata @@ -30,30 +28,26 @@ def _encode(self, metadata: UploadMetadata) -> str: ] return ",".join(res) - def file_exists(self, filename) -> bool: - """Verify if the file exists in the storage - Parameters - ---------- - filename - This could be the local filename or objectname in the storage - """ - return filename in self._filestore.storage - def get_link(self, objectname) -> str: """Get the link associated with objectname in the bucket + Parameters ---------- objectname This could be the local filename or objectname in the storage """ - if not self.file_exists(objectname): - raise StorageException(f"There is no entry for {objectname} in FileStore") + if not self.is_valid_arg(objectname): + raise StorageException("bucketname cannot be empty") return self._filestore.get_link(objectname) + def is_valid_arg(self, target): + return target is not None and isinstance(target, str) and len(target.strip()) != 0 + async def create_unique_link( self, bucketname=None, objectname=None, filename=None ) -> None: """Create unique link according to bucketname and objectname + Parameters ---------- bucketname @@ -63,17 +57,19 @@ async def create_unique_link( filename Local file """ - if not self._is_valid_arg(bucketname): - raise StorageException("Bucketname cannot be empty") + if not self.is_valid_arg(bucketname): + raise StorageException("bucketname cannot be empty") - if not (self._is_valid_arg(objectname) or self._is_valid_arg(filename)): + if not (self.is_valid_arg(objectname) or self.is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") file = filename if filename else objectname - if not self._is_valid_arg(file): + + if not self.is_valid_arg(file): raise StorageException("Must specify objectname or filename") upload_mode = None + info = FileInfo( name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} ) @@ -113,6 +109,7 @@ async def create_unique_link( async def resumable_offset(self, link, headers) -> str: """Get the current offset to be used + Parameters ---------- link @@ -120,7 +117,15 @@ async def resumable_offset(self, link, headers) -> str: headers Metadata headers sent to the server """ + + if not self._filestore.link_exists(link): + raise StorageException(f"There's no a reference to that link: {link}") + response = await self._client.head(link, headers=headers) + + if "upload-offset" not in response.headers: + raise StorageException("Error while fetching the next offset.") + return response.headers["upload-offset"] async def terminate(self, file: str) -> None: @@ -131,6 +136,9 @@ async def terminate(self, file: str) -> None: file file name used to get its metadata info """ + if not self.is_valid_arg(file): + raise StorageException("file argument cannot be empty") + info = self._filestore.get_file_info(file) response = await self._client.delete(info["link"], headers=info["headers"]) @@ -143,6 +151,7 @@ async def upload( self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 ) -> None: """Send file's content in chunks to the target url + Parameters ---------- filename @@ -157,21 +166,21 @@ async def upload( Amount of megabytes to be sent in each iteration """ if upload_defer: - if not (self._is_valid_arg(link) and self._is_valid_arg(objectname)): + if not (self.is_valid_arg(link) and self.is_valid_arg(objectname)): raise StorageException( "Upload-Defer mode requires a link and objectname" ) - if not self._is_valid_arg(filename): + if not self.is_valid_arg(filename): raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename - chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size + chunk_size = 1048576 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" ) - storage_link = link if upload_defer else self._filestore.get_link(target_file) + storage_link = link if upload_defer else self.get_link(target_file) if upload_defer: size = str(os.stat(filename).st_size) @@ -193,9 +202,11 @@ async def upload( chunk = file.read(chunk_size) headers = self._filestore.get_file_headers(target_file) + response = await self._client.patch( storage_link, headers=headers, content=chunk ) + if response.status_code not in {201, 204}: raise StorageException(response.content) From f529d512e5a79a6c9cb51d50e2b72aa3fd69154d Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:44:15 -0300 Subject: [PATCH 59/88] fix: Add more tests --- storage3/_sync/resumable.py | 56 ++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index a2a74376..92e64a79 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -5,7 +5,7 @@ from ..types import FileInfo, UploadMetadata from ..utils import FileStore, StorageException, SyncClient -__all__ = ("ResumableUpload",) +__all__ = ["ResumableUpload"] class ResumableUpload: @@ -15,11 +15,12 @@ def __init__(self, session: SyncClient) -> None: self.expiration_time_format = "%a, %d %b %Y %X %Z" self._filestore = FileStore() - def _is_valid_arg(self, target): - return isinstance(target, str) and len(target.strip()) > 0 + def is_valid_arg(self, target): + return target is not None and isinstance(target, str) and len(target.strip()) != 0 def _encode(self, metadata: UploadMetadata) -> str: """Generate base64 encoding for Upload-Metadata header + Parameters ---------- metadata @@ -30,30 +31,23 @@ def _encode(self, metadata: UploadMetadata) -> str: ] return ",".join(res) - def file_exists(self, filename) -> bool: - """Verify if the file exists in the storage - Parameters - ---------- - filename - This could be the local filename or objectname in the storage - """ - return filename in self._filestore.storage - - def get_link(self, objectname) -> str: + def get_link(self, objectname: str) -> str: """Get the link associated with objectname in the bucket + Parameters ---------- objectname This could be the local filename or objectname in the storage """ - if not self.file_exists(objectname): - raise StorageException(f"There is no entry for {objectname} in FileStore") + if not self.is_valid_arg(objectname): + raise StorageException("bucketname cannot be empty") return self._filestore.get_link(objectname) def create_unique_link( self, bucketname=None, objectname=None, filename=None ) -> None: """Create unique link according to bucketname and objectname + Parameters ---------- bucketname @@ -63,17 +57,19 @@ def create_unique_link( filename Local file """ - if not self._is_valid_arg(bucketname): + if not self.is_valid_arg(bucketname): raise StorageException("bucketname cannot be empty") - if not (self._is_valid_arg(objectname) or self._is_valid_arg(filename)): + if not (self.is_valid_arg(objectname) or self.is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") file = filename if filename else objectname - if not self._is_valid_arg(file): + + if not self.is_valid_arg(file): raise StorageException("Must specify objectname or filename") upload_mode = None + info = FileInfo( name=file, link="", length="", headers={"Tus-Resumable": "1.0.0"} ) @@ -111,8 +107,9 @@ def create_unique_link( del info["headers"][upload_mode] self._filestore.mark_file(info) - def resumable_offset(self, link, headers) -> str: + def resumable_offset(self, link: str, headers) -> str: """Get the current offset to be used + Parameters ---------- link @@ -120,7 +117,15 @@ def resumable_offset(self, link, headers) -> str: headers Metadata headers sent to the server """ + + if not self._filestore.link_exists(link): + raise StorageException(f"There's no a reference to that link: {link}") + response = self._client.head(link, headers=headers) + + if "upload-offset" not in response.headers: + raise StorageException("Error while fetching the next offset.") + return response.headers["upload-offset"] def terminate(self, file: str) -> None: @@ -131,6 +136,9 @@ def terminate(self, file: str) -> None: file file name used to get its metadata info """ + if not self.is_valid_arg(file): + raise StorageException("file argument cannot be empty") + info = self._filestore.get_file_info(file) response = self._client.delete(info["link"], headers=info["headers"]) @@ -143,6 +151,7 @@ def upload( self, filename, upload_defer=False, link=None, objectname=None, mb_size=1 ) -> None: """Send file's content in chunks to the target url + Parameters ---------- filename @@ -156,22 +165,23 @@ def upload( mb_size Amount of megabytes to be sent in each iteration """ + if upload_defer: - if not (self._is_valid_arg(link) and self._is_valid_arg(objectname)): + if not (self.is_valid_arg(link) and self.is_valid_arg(objectname)): raise StorageException( "Upload-Defer mode requires a link and objectname" ) - if not self._is_valid_arg(filename): + if not self.is_valid_arg(filename): raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename - chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size + chunk_size = 1048576 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" ) - storage_link = link if upload_defer else self._filestore.get_link(target_file) + storage_link = link if upload_defer else self.get_link(target_file) if upload_defer: size = str(os.stat(filename).st_size) From e8682e0f634a5f4c5a0d7844d1672622ecb2dd6d Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:44:27 -0300 Subject: [PATCH 60/88] fix: Add more tests --- tests/resumable/test_resumable.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index e99a77b1..7fff3df7 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -2,6 +2,46 @@ from conftest import is_https_url +from storage3.utils import StorageException + + +def test_non_valid_resumable_options(sync_client): + client = sync_client + + """Raise an exception when argument is not a string""" + try: + client.resumable.resumable_offset(1, {}) + except Exception as e: + assert isinstance(e, StorageException) + + """Raise an exception when argument is an empty string""" + try: + client.resumable.resumable_offset("https://random_bucket_id_link", {}) + except Exception as e: + assert isinstance(e, StorageException) + + +def test_non_valid_terminate_options(sync_client): + client = sync_client + + """Raise an exception when argument is not a string""" + try: + client.resumable.terminate(1) + except Exception as e: + assert isinstance(e, StorageException) + + """Raise an exception when argument is an empty string""" + try: + client.resumable.terminate(" ") + except Exception as e: + assert isinstance(e, StorageException) + + """Raise an exception when there's no a fileinfo associated with the argument passed""" + try: + client.resumable.terminate("random_🐍.log") + except Exception as e: + assert isinstance(e, StorageException) + def test_sync_client(sync_client, file, test_bucket): client = sync_client @@ -28,6 +68,11 @@ def test_sync_client(sync_client, file, test_bucket): is_file_loaded = any(item["name"] == file.name for item in bucket.list()) assert is_file_loaded, f"File not loaded:\n{bucket.list()}" + try: + client.resumable.terminate("") + except Exception as e: + assert isinstance(e, StorageException) + bucket.remove(file.name) From f3e50a647b9d99c9dcbb2096bb43e1628f30bdbd Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:47:39 -0300 Subject: [PATCH 61/88] fix: Add more tests --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ac38f83b..8963e0b8 100644 --- a/.gitignore +++ b/.gitignore @@ -284,3 +284,7 @@ venv.bak/ .mypy_cache/ .dmypy.json dmypy.json + + +# For testing purposes only. +test_image.svg From 03773c71ba2b731df61873d4e3685042c1436868 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:56:15 -0300 Subject: [PATCH 62/88] fix: Style --- storage3/_async/resumable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 87754cd7..76f75eac 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -41,7 +41,9 @@ def get_link(self, objectname) -> str: return self._filestore.get_link(objectname) def is_valid_arg(self, target): - return target is not None and isinstance(target, str) and len(target.strip()) != 0 + return ( + target is not None and isinstance(target, str) and len(target.strip()) != 0 + ) async def create_unique_link( self, bucketname=None, objectname=None, filename=None From d5a7abe37e3b3f5e0f84c06794760db45c3be1ea Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:56:20 -0300 Subject: [PATCH 63/88] fix: Style --- storage3/_sync/resumable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 92e64a79..41e8d80b 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -16,7 +16,9 @@ def __init__(self, session: SyncClient) -> None: self._filestore = FileStore() def is_valid_arg(self, target): - return target is not None and isinstance(target, str) and len(target.strip()) != 0 + return ( + target is not None and isinstance(target, str) and len(target.strip()) != 0 + ) def _encode(self, metadata: UploadMetadata) -> str: """Generate base64 encoding for Upload-Metadata header From e60539ac646102f1a5572672c04790dad5963316 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 16:57:42 -0300 Subject: [PATCH 64/88] fix: Grammar --- tests/resumable/test_resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index 7fff3df7..5171f62a 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -36,7 +36,7 @@ def test_non_valid_terminate_options(sync_client): except Exception as e: assert isinstance(e, StorageException) - """Raise an exception when there's no a fileinfo associated with the argument passed""" + """Raise an exception when there's no fileinfo associated with the argument passed""" try: client.resumable.terminate("random_🐍.log") except Exception as e: From 3925e9dbadce573cc06b72ae9fad65324888fb47 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:02:16 -0300 Subject: [PATCH 65/88] fix: Style --- storage3/_async/resumable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 76f75eac..3fbcb167 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -5,7 +5,8 @@ from ..types import FileInfo, UploadMetadata from ..utils import AsyncClient, FileStore, StorageException -__all__ = ["AsyncResumableUpload"] + +__all__ = ("AsyncResumableUpload", ) class AsyncResumableUpload: From 92cf2ac37188fc8867c44ae863acafb3d0c13f93 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:02:22 -0300 Subject: [PATCH 66/88] fix: Style --- storage3/_sync/resumable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 41e8d80b..eba46cb4 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -5,7 +5,8 @@ from ..types import FileInfo, UploadMetadata from ..utils import FileStore, StorageException, SyncClient -__all__ = ["ResumableUpload"] + +__all__ = ("ResumableUpload", ) class ResumableUpload: From ca69c12e6941e4b0ea135cdef27facc8e345f5d8 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:08:02 -0300 Subject: [PATCH 67/88] fix: Style --- storage3/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/storage3/utils.py b/storage3/utils.py index bfbadd0c..dd3774d6 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -150,6 +150,13 @@ def open_file(self, filename: str, offset: int): return file def close_file(self, filename) -> None: + """Close the file. + + Parameters + ---------- + filename + key name referencing to filename attributes. + """ filename.close() def remove_file(self, filename: str) -> None: @@ -187,6 +194,6 @@ def link_exists(self, link: str) -> bool: Parameters ---------- link: - link associated with a resumable endpoint + link associated with a resumable endpoint """ return any(self.get_link(obj) == link for obj in self.storage.keys()) From 0eabc1b9279e3fa0194b264b42fb9ad088755d62 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:08:07 -0300 Subject: [PATCH 68/88] fix: Style --- storage3/_sync/resumable.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index eba46cb4..192a6f45 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -5,8 +5,7 @@ from ..types import FileInfo, UploadMetadata from ..utils import FileStore, StorageException, SyncClient - -__all__ = ("ResumableUpload", ) +__all__ = ("ResumableUpload",) class ResumableUpload: From c831866efc8943b69be4fbcff20e78c805dc9a65 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:08:12 -0300 Subject: [PATCH 69/88] fix: Style --- storage3/_async/resumable.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 3fbcb167..0a06e233 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -5,8 +5,7 @@ from ..types import FileInfo, UploadMetadata from ..utils import AsyncClient, FileStore, StorageException - -__all__ = ("AsyncResumableUpload", ) +__all__ = ("AsyncResumableUpload",) class AsyncResumableUpload: From ddf99f2b41d2252442076d1abacf963b27588d52 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:08:16 -0300 Subject: [PATCH 70/88] fix: Style --- tests/resumable/conftest.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 7e3693d4..b1e8f5ad 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -18,19 +18,19 @@ def is_https_url(url: str) -> bool: @pytest.fixture def file() -> str: file_name = "test_image.svg" - file_content = ( - b' ' - b' ' - b' ' - b' ' - b' ' - ) + file_content = b""" + + + + + + """.strip() with open(file_name, "wb") as f: f.write(file_content) From 952aa7b84a7ba67c651fdd4dd39d0ba9d3ea113c Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:25:02 -0300 Subject: [PATCH 71/88] fix: Add comments --- tests/resumable/conftest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index b1e8f5ad..4f94778f 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -11,13 +11,15 @@ def is_https_url(url: str) -> bool: - """Simple helper for testing purposes.""" + """Simple helper that checks if string argument is an HTTPS URL.""" return urlparse(url).scheme == "https" @pytest.fixture def file() -> str: + """Simple helper that writes an SVG file.""" file_name = "test_image.svg" + # Supabase logo SVG, for testing purposes only. file_content = b""" SyncStorageClient: + """Simple helper that returns an SyncStorageClient.""" url, key = configure_client client = SyncStorageClient(url, {"apiKey": key, "Authorization": f"Bearer {key}"}) return client @@ -65,6 +70,7 @@ def sync_client(configure_client) -> SyncStorageClient: @pytest.fixture(scope="module") def async_client(configure_client) -> AsyncStorageClient: + """Simple helper that returns an AsyncStorageClient.""" url, key = configure_client client = AsyncStorageClient(url, {"apiKey": key, "Authorization": f"Bearer {key}"}) return client From 5e889362202ff8008353359d2484616504d2be9f Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 21 Oct 2024 17:25:49 -0300 Subject: [PATCH 72/88] fix: Add comments --- tests/resumable/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resumable/conftest.py b/tests/resumable/conftest.py index 4f94778f..2ce79a99 100644 --- a/tests/resumable/conftest.py +++ b/tests/resumable/conftest.py @@ -54,7 +54,7 @@ def test_bucket() -> str: @pytest.fixture(scope="module") def configure_client(): - """Get URL and API Key from env args.""" + """Get API URL and API Key from env args.""" url = f'{os.getenv("SUPABASE_URL")}/storage/v1' key = os.getenv("SUPABASE_KEY") return (url, key) From c029d28e4d8e40e73c45accf45d070c513119e74 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 16:35:47 -0300 Subject: [PATCH 73/88] fix: style --- storage3/_async/resumable.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 0a06e233..8ade46c1 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -37,7 +37,7 @@ def get_link(self, objectname) -> str: This could be the local filename or objectname in the storage """ if not self.is_valid_arg(objectname): - raise StorageException("bucketname cannot be empty") + raise StorageException("Bucketname cannot be empty") return self._filestore.get_link(objectname) def is_valid_arg(self, target): @@ -60,7 +60,7 @@ async def create_unique_link( Local file """ if not self.is_valid_arg(bucketname): - raise StorageException("bucketname cannot be empty") + raise StorageException("Bucketname cannot be empty") if not (self.is_valid_arg(objectname) or self.is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") @@ -121,7 +121,7 @@ async def resumable_offset(self, link, headers) -> str: """ if not self._filestore.link_exists(link): - raise StorageException(f"There's no a reference to that link: {link}") + raise StorageException(f"There's no reference to that link: {link}") response = await self._client.head(link, headers=headers) @@ -139,7 +139,7 @@ async def terminate(self, file: str) -> None: file name used to get its metadata info """ if not self.is_valid_arg(file): - raise StorageException("file argument cannot be empty") + raise StorageException("File argument cannot be empty") info = self._filestore.get_file_info(file) response = await self._client.delete(info["link"], headers=info["headers"]) @@ -177,7 +177,7 @@ async def upload( raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename - chunk_size = 1048576 * mb_size + chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" From 41e59154315111b5789a02c1938df262302b8eb2 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 16:35:52 -0300 Subject: [PATCH 74/88] fix: style --- storage3/_sync/resumable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 192a6f45..03d527e2 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -42,7 +42,7 @@ def get_link(self, objectname: str) -> str: This could be the local filename or objectname in the storage """ if not self.is_valid_arg(objectname): - raise StorageException("bucketname cannot be empty") + raise StorageException("Bucketname cannot be empty") return self._filestore.get_link(objectname) def create_unique_link( @@ -60,7 +60,7 @@ def create_unique_link( Local file """ if not self.is_valid_arg(bucketname): - raise StorageException("bucketname cannot be empty") + raise StorageException("Bucketname cannot be empty") if not (self.is_valid_arg(objectname) or self.is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") @@ -139,7 +139,7 @@ def terminate(self, file: str) -> None: file name used to get its metadata info """ if not self.is_valid_arg(file): - raise StorageException("file argument cannot be empty") + raise StorageException("File argument cannot be empty") info = self._filestore.get_file_info(file) response = self._client.delete(info["link"], headers=info["headers"]) @@ -178,7 +178,7 @@ def upload( raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename - chunk_size = 1048576 * mb_size + chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" From 005acebe78608d4a987d044392604e025fba8a92 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 17:13:41 -0300 Subject: [PATCH 75/88] fix: simplify --- storage3/utils.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/storage3/utils.py b/storage3/utils.py index dd3774d6..d1a2cd48 100644 --- a/storage3/utils.py +++ b/storage3/utils.py @@ -1,6 +1,7 @@ import json import os import tempfile +from base64 import b64encode from datetime import datetime from hashlib import md5 from typing import Dict @@ -8,7 +9,7 @@ from httpx import AsyncClient as AsyncClient # noqa: F401 from httpx import Client as BaseClient -from .types import FileInfo +from .types import FileInfo, UploadMetadata class SyncClient(BaseClient): @@ -197,3 +198,19 @@ def link_exists(self, link: str) -> bool: link associated with a resumable endpoint """ return any(self.get_link(obj) == link for obj in self.storage.keys()) + + +def is_valid_arg(target: str) -> bool: + return target is not None and isinstance(target, str) and len(target.strip()) != 0 + + +def base64encode_metadata(metadata: UploadMetadata) -> str: + """Generate base64 encoding for Upload-Metadata header + + Parameters + ---------- + metadata + Bucket and object pair representing the resulting file in the storage + """ + res = [f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items()] + return ",".join(res) From 1e2d293d432db89e634893eec594e6d74c5c3fdf Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 17:13:46 -0300 Subject: [PATCH 76/88] fix: simplify --- storage3/_async/resumable.py | 43 +++++++++++++----------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index 8ade46c1..e4df4b10 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -1,9 +1,14 @@ import os -from base64 import b64encode from datetime import datetime from ..types import FileInfo, UploadMetadata -from ..utils import AsyncClient, FileStore, StorageException +from ..utils import ( + AsyncClient, + FileStore, + StorageException, + base64encode_metadata, + is_valid_arg, +) __all__ = ("AsyncResumableUpload",) @@ -15,19 +20,6 @@ def __init__(self, session: AsyncClient) -> None: self.expiration_time_format = "%a, %d %b %Y %X %Z" self._filestore = FileStore() - def _encode(self, metadata: UploadMetadata) -> str: - """Generate base64 encoding for Upload-Metadata header - - Parameters - ---------- - metadata - Bucket and object pair representing the resulting file in the storage - """ - res = [ - f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items() - ] - return ",".join(res) - def get_link(self, objectname) -> str: """Get the link associated with objectname in the bucket @@ -36,15 +28,10 @@ def get_link(self, objectname) -> str: objectname This could be the local filename or objectname in the storage """ - if not self.is_valid_arg(objectname): + if not is_valid_arg(objectname): raise StorageException("Bucketname cannot be empty") return self._filestore.get_link(objectname) - def is_valid_arg(self, target): - return ( - target is not None and isinstance(target, str) and len(target.strip()) != 0 - ) - async def create_unique_link( self, bucketname=None, objectname=None, filename=None ) -> None: @@ -59,15 +46,15 @@ async def create_unique_link( filename Local file """ - if not self.is_valid_arg(bucketname): + if not is_valid_arg(bucketname): raise StorageException("Bucketname cannot be empty") - if not (self.is_valid_arg(objectname) or self.is_valid_arg(filename)): + if not (is_valid_arg(objectname) or is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") file = filename if filename else objectname - if not self.is_valid_arg(file): + if not is_valid_arg(file): raise StorageException("Must specify objectname or filename") upload_mode = None @@ -94,7 +81,7 @@ async def create_unique_link( obj_name = os.path.split(file)[1] metadata = UploadMetadata(bucketName=bucketname, objectName=obj_name) - info["headers"]["Upload-Metadata"] = self._encode(metadata) + info["headers"]["Upload-Metadata"] = base64encode_metadata(metadata) response = await self._client.post(self.url, headers=info["headers"]) if response.status_code != 201: @@ -138,7 +125,7 @@ async def terminate(self, file: str) -> None: file file name used to get its metadata info """ - if not self.is_valid_arg(file): + if not is_valid_arg(file): raise StorageException("File argument cannot be empty") info = self._filestore.get_file_info(file) @@ -168,12 +155,12 @@ async def upload( Amount of megabytes to be sent in each iteration """ if upload_defer: - if not (self.is_valid_arg(link) and self.is_valid_arg(objectname)): + if not (is_valid_arg(link) and is_valid_arg(objectname)): raise StorageException( "Upload-Defer mode requires a link and objectname" ) - if not self.is_valid_arg(filename): + if not is_valid_arg(filename): raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename From 671b545602c117af9d8e3fefbd8ddb371bae7ca9 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 17:13:52 -0300 Subject: [PATCH 77/88] fix: simplify --- storage3/_sync/resumable.py | 43 +++++++++++++------------------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 03d527e2..067a50f6 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -1,9 +1,14 @@ import os -from base64 import b64encode from datetime import datetime from ..types import FileInfo, UploadMetadata -from ..utils import FileStore, StorageException, SyncClient +from ..utils import ( + FileStore, + StorageException, + SyncClient, + base64encode_metadata, + is_valid_arg, +) __all__ = ("ResumableUpload",) @@ -15,24 +20,6 @@ def __init__(self, session: SyncClient) -> None: self.expiration_time_format = "%a, %d %b %Y %X %Z" self._filestore = FileStore() - def is_valid_arg(self, target): - return ( - target is not None and isinstance(target, str) and len(target.strip()) != 0 - ) - - def _encode(self, metadata: UploadMetadata) -> str: - """Generate base64 encoding for Upload-Metadata header - - Parameters - ---------- - metadata - Bucket and object pair representing the resulting file in the storage - """ - res = [ - f"{k} {b64encode(bytes(v, 'utf-8')).decode()}" for k, v in metadata.items() - ] - return ",".join(res) - def get_link(self, objectname: str) -> str: """Get the link associated with objectname in the bucket @@ -41,7 +28,7 @@ def get_link(self, objectname: str) -> str: objectname This could be the local filename or objectname in the storage """ - if not self.is_valid_arg(objectname): + if not is_valid_arg(objectname): raise StorageException("Bucketname cannot be empty") return self._filestore.get_link(objectname) @@ -59,15 +46,15 @@ def create_unique_link( filename Local file """ - if not self.is_valid_arg(bucketname): + if not is_valid_arg(bucketname): raise StorageException("Bucketname cannot be empty") - if not (self.is_valid_arg(objectname) or self.is_valid_arg(filename)): + if not (is_valid_arg(objectname) or is_valid_arg(filename)): raise StorageException("Must specify objectname or filename") file = filename if filename else objectname - if not self.is_valid_arg(file): + if not is_valid_arg(file): raise StorageException("Must specify objectname or filename") upload_mode = None @@ -94,7 +81,7 @@ def create_unique_link( obj_name = os.path.split(file)[1] metadata = UploadMetadata(bucketName=bucketname, objectName=obj_name) - info["headers"]["Upload-Metadata"] = self._encode(metadata) + info["headers"]["Upload-Metadata"] = base64encode_metadata(metadata) response = self._client.post(self.url, headers=info["headers"]) if response.status_code != 201: @@ -138,7 +125,7 @@ def terminate(self, file: str) -> None: file file name used to get its metadata info """ - if not self.is_valid_arg(file): + if not is_valid_arg(file): raise StorageException("File argument cannot be empty") info = self._filestore.get_file_info(file) @@ -169,12 +156,12 @@ def upload( """ if upload_defer: - if not (self.is_valid_arg(link) and self.is_valid_arg(objectname)): + if not (is_valid_arg(link) and is_valid_arg(objectname)): raise StorageException( "Upload-Defer mode requires a link and objectname" ) - if not self.is_valid_arg(filename): + if not is_valid_arg(filename): raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename From 438ad8b903fdd02591b8c5f9f70372ebbf6ca1b5 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 17:53:00 -0300 Subject: [PATCH 78/88] fix: style --- storage3/_async/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/_async/resumable.py b/storage3/_async/resumable.py index e4df4b10..909746c2 100644 --- a/storage3/_async/resumable.py +++ b/storage3/_async/resumable.py @@ -164,7 +164,7 @@ async def upload( raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename - chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size + chunk_size = 1048576 * int(max(1, mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" From aeb0244396edfcb1da50dffe335b5534172f3bb9 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 22 Oct 2024 17:53:05 -0300 Subject: [PATCH 79/88] fix: style --- storage3/_sync/resumable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage3/_sync/resumable.py b/storage3/_sync/resumable.py index 067a50f6..75228107 100644 --- a/storage3/_sync/resumable.py +++ b/storage3/_sync/resumable.py @@ -165,7 +165,7 @@ def upload( raise StorageException("Must specify a filename") target_file = objectname if upload_defer else filename - chunk_size = 1048576 * int(abs(mb_size)) # 1024 * 1024 * mb_size + chunk_size = 1048576 * int(max(1, mb_size)) # 1024 * 1024 * mb_size size = None self._filestore.update_file_headers( target_file, "Content-Type", "application/offset+octet-stream" From 279882efcbb242bca5e3301aa34408f50b61c8a4 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 23 Oct 2024 10:29:12 -0300 Subject: [PATCH 80/88] fix: add documentation --- tests/resumable/README.md | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/resumable/README.md diff --git a/tests/resumable/README.md b/tests/resumable/README.md new file mode 100644 index 00000000..96dfe1f1 --- /dev/null +++ b/tests/resumable/README.md @@ -0,0 +1,59 @@ +# Tests for Resumable + +- How to run tests? + + +### Create a new project + +- Create 1 new empty project. + + +### Create a new bucket + +- Create 1 new empty bucket named "test". + + +### Create policies + +- Create policies to allow read/write operations into the bucket. +- Policies should grant read access into `storage.objects` and `storage.buckets`. +- Policies should grant insert (upload) and delete access of resources. +- Set temporary promiscuous permissions for testing purposes only (delete it after tests). + + +#### Policies templates + +- Example templates for policies, **must be edited before use** to fit your use case. + +At `storage.objects`: + +```sql +CREATE POLICY "Enable all access for all users" ON "storage"."objects" +AS PERMISSIVE FOR ALL +TO public +USING (true) +WITH CHECK (true); +``` + +At `storage.buckets`: + +```sql +CREATE POLICY "Enable all access for all users" ON "storage"."buckets" +AS PERMISSIVE FOR ALL +TO public +USING (true) +WITH CHECK (true); +``` + + +### Run tests + +```bash +export SUPABASE_URL = 'Supabase_HTTPS_URL_here' +export SUPABASE_KEY = 'Supabase_API_Key_here' +export TEST_BUCKET = 'test' # BucketName + +poetry run pytest tests/resumable -v -s +``` + +- You can delete the bucket and policies after running the tests. From dfc7ad561d37967355a1fe884800c33edd99a3c2 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 23 Oct 2024 16:14:39 -0300 Subject: [PATCH 81/88] fix: add more documentation --- tests/resumable/create_new_bucket.png | Bin 0 -> 46496 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resumable/create_new_bucket.png diff --git a/tests/resumable/create_new_bucket.png b/tests/resumable/create_new_bucket.png new file mode 100644 index 0000000000000000000000000000000000000000..f2febf274bece8cd82648484358cad680f4f9315 GIT binary patch literal 46496 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 Date: Wed, 23 Oct 2024 16:14:48 -0300 Subject: [PATCH 82/88] fix: add more documentation --- tests/resumable/env_variables.png | Bin 0 -> 101057 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resumable/env_variables.png diff --git a/tests/resumable/env_variables.png b/tests/resumable/env_variables.png new file mode 100644 index 0000000000000000000000000000000000000000..ccc853e88b505c852f4761819d0cf04e0d5819e5 GIT binary patch literal 101057 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 bF@V9w8nG=!N53kSIE%(H?V~B`<`4b{u4t1e literal 0 HcmV?d00001 From 60b98ddcc55de586ff22f3a9bfb9e29c407ff3a1 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 23 Oct 2024 16:14:54 -0300 Subject: [PATCH 83/88] fix: add more documentation --- tests/resumable/policies.png | Bin 0 -> 40238 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resumable/policies.png diff --git a/tests/resumable/policies.png b/tests/resumable/policies.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c4416a33641db66a6f9094f29fb593634b65de GIT binary patch literal 40238 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

F6jxLn=b?`MV?fEXp^7*CV75@xcxHiPY{nih<^Y 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�wjEuqRd2Ry!r_UUMAYMVu{vAcvu7|j0RZ+1 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 dBQwid`Kw^d3fJD~;8g(UjO Date: Wed, 23 Oct 2024 16:14:59 -0300 Subject: [PATCH 84/88] fix: add more documentation --- tests/resumable/policies_list.png | Bin 0 -> 59833 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resumable/policies_list.png diff --git a/tests/resumable/policies_list.png b/tests/resumable/policies_list.png new file mode 100644 index 0000000000000000000000000000000000000000..b2e784545955b9dcf4b02290060229c71715319e GIT binary patch literal 59833 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 literal 0 HcmV?d00001 From c2bd3c5a82c3010fa7ddb22801a0de05ffae1a75 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Wed, 23 Oct 2024 16:15:09 -0300 Subject: [PATCH 85/88] fix: add more documentation --- tests/resumable/README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/resumable/README.md b/tests/resumable/README.md index 96dfe1f1..a5ec2f3b 100644 --- a/tests/resumable/README.md +++ b/tests/resumable/README.md @@ -5,13 +5,15 @@ ### Create a new project -- Create 1 new empty project. +- Create 1 new empty project from the [Dashboard](https://supabase.com/dashboard/projects). ### Create a new bucket - Create 1 new empty bucket named "test". +![](create_new_bucket.png) + ### Create policies @@ -20,6 +22,11 @@ - Policies should grant insert (upload) and delete access of resources. - Set temporary promiscuous permissions for testing purposes only (delete it after tests). +![](policies.png) + + +![](policies_list.png) + #### Policies templates @@ -46,13 +53,20 @@ WITH CHECK (true); ``` -### Run tests +### Set environment variables + +![](env_variables.png) ```bash export SUPABASE_URL = 'Supabase_HTTPS_URL_here' export SUPABASE_KEY = 'Supabase_API_Key_here' export TEST_BUCKET = 'test' # BucketName +``` + +### Run tests + +```bash poetry run pytest tests/resumable -v -s ``` From c5e8caf96801e45896770f4673e9ba8fc9d227ed Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 28 Oct 2024 10:14:44 -0300 Subject: [PATCH 86/88] fix: Fix conflicts with main, resync --- tests/resumable/test_resumable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/resumable/test_resumable.py b/tests/resumable/test_resumable.py index 5171f62a..7e123e99 100644 --- a/tests/resumable/test_resumable.py +++ b/tests/resumable/test_resumable.py @@ -63,7 +63,7 @@ def test_sync_client(sync_client, file, test_bucket): """Verify if the file was loaded correctly""" client.resumable.upload(file.name) - bucket = client.get_bucket(test_bucket) + bucket = client.from_(test_bucket) is_file_loaded = any(item["name"] == file.name for item in bucket.list()) assert is_file_loaded, f"File not loaded:\n{bucket.list()}" @@ -99,7 +99,7 @@ def test_deferred_sync_client(sync_client, file, test_bucket): client.resumable.upload( file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name ) - bucket = client.get_bucket(test_bucket) + bucket = client.from_(test_bucket) is_file_loaded = any(item["name"] == file.name for item in bucket.list()) assert is_file_loaded, f"File not loaded:\n{bucket.list()}" @@ -129,7 +129,7 @@ async def test_async_client(async_client, file, test_bucket): """Verify if the file was loaded correctly""" await client.resumable.upload(file.name) - bucket = await client.get_bucket(test_bucket) + bucket = client.from_(test_bucket) is_file_loaded = any(item["name"] == file.name for item in await bucket.list()) assert is_file_loaded, f"File not loaded:\n{bucket.list()}" @@ -162,7 +162,7 @@ async def test_deferred_async_client(async_client, file, test_bucket): await client.resumable.upload( file.name, mb_size=10, upload_defer=True, link=link, objectname=file.name ) - bucket = await client.get_bucket(test_bucket) + bucket = client.from_(test_bucket) is_file_loaded = any(item["name"] == file.name for item in await bucket.list()) assert is_file_loaded, f"File not loaded:\n{bucket.list()}" From 1633e381f8cdef074ad66bf86c335f7a4a52cb4a Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 28 Oct 2024 10:19:47 -0300 Subject: [PATCH 87/88] fix: Resync From 59790f9d085d4d533507ee7758efd0879e08a880 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Mon, 28 Oct 2024 10:24:16 -0300 Subject: [PATCH 88/88] fix: Resync