diff --git a/src/jcloud_s3fuse/s3/backend.py b/src/jcloud_s3fuse/s3/backend.py new file mode 100644 index 0000000..33f4c86 --- /dev/null +++ b/src/jcloud_s3fuse/s3/backend.py @@ -0,0 +1,62 @@ +# Copyright 2026 jCloud + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import httpx +from typing import AsyncGenerator, Optional +from urllib.parse import urljoin + +__all__ = [ + 'S3Backend' +] + +class S3Backend: + ''' + A class representing an S3 compatible backend. + + :param url: The URL of the backend. + :type url: str + ''' + + def __init__(self, url: str) -> None: + self.url = url + + async def get_object(self, object_path: str, range: Optional[str] = None) -> AsyncGenerator[bytes]: + ''' + Returns the bytes of an object. + + :param object_path: The path of the object. It consists of the + bucket name and the object key. + :type object_path: str + :param range: The bytes range as an RFC 9110-compliant string. + :type range: Optional[str] + + :return: An iterator yielding the object bytes. + :rtype: AsyncGenerator[bytes] + ''' + + if range is not None: + headers = { + 'Range': f'bytes={range}' + } + else: + headers = {} + + async with httpx.AsyncClient() as client: + async with client.stream( + 'GET', + urljoin(self.url, object_path), + headers = headers + ) as response: + async for chunk in response.aiter_bytes(): + yield chunk