feat(backup): wrapper de stockage S3 (boto3) + faux stockage de test (#89)
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
from src.backup.config import BackupConfig
|
||||||
|
|
||||||
|
|
||||||
|
class Storage(Protocol):
|
||||||
|
def upload_bytes(self, key: str, data: bytes) -> None: ...
|
||||||
|
def download_bytes(self, key: str) -> bytes: ...
|
||||||
|
def list_keys(self, prefix: str) -> list[str]: ...
|
||||||
|
def delete(self, key: str) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class S3Storage:
|
||||||
|
def __init__(
|
||||||
|
self, bucket, endpoint_url, access_key, secret_key, region="us-east-1"
|
||||||
|
):
|
||||||
|
self._bucket = bucket
|
||||||
|
self._client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=endpoint_url,
|
||||||
|
aws_access_key_id=access_key,
|
||||||
|
aws_secret_access_key=secret_key,
|
||||||
|
region_name=region,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config: BackupConfig) -> "S3Storage":
|
||||||
|
return cls(
|
||||||
|
bucket=config.bucket,
|
||||||
|
endpoint_url=config.endpoint_url,
|
||||||
|
access_key=config.access_key,
|
||||||
|
secret_key=config.secret_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
def upload_bytes(self, key: str, data: bytes) -> None:
|
||||||
|
self._client.put_object(Bucket=self._bucket, Key=key, Body=data)
|
||||||
|
|
||||||
|
def download_bytes(self, key: str) -> bytes:
|
||||||
|
resp = self._client.get_object(Bucket=self._bucket, Key=key)
|
||||||
|
return resp["Body"].read()
|
||||||
|
|
||||||
|
def list_keys(self, prefix: str) -> list[str]:
|
||||||
|
keys: list[str] = []
|
||||||
|
paginator = self._client.get_paginator("list_objects_v2")
|
||||||
|
for page in paginator.paginate(Bucket=self._bucket, Prefix=prefix):
|
||||||
|
for obj in page.get("Contents", []):
|
||||||
|
keys.append(obj["Key"])
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
self._client.delete_object(Bucket=self._bucket, Key=key)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class FakeStorage:
|
||||||
|
def __init__(self):
|
||||||
|
self.objects: dict[str, bytes] = {}
|
||||||
|
|
||||||
|
def upload_bytes(self, key, data):
|
||||||
|
self.objects[key] = data
|
||||||
|
|
||||||
|
def download_bytes(self, key):
|
||||||
|
return self.objects[key]
|
||||||
|
|
||||||
|
def list_keys(self, prefix):
|
||||||
|
return [k for k in self.objects if k.startswith(prefix)]
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
self.objects.pop(key, None)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import boto3
|
||||||
|
import pytest
|
||||||
|
from moto import mock_aws
|
||||||
|
|
||||||
|
from src.backup.storage import S3Storage
|
||||||
|
|
||||||
|
BUCKET = "decp-backups"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def s3_storage():
|
||||||
|
with mock_aws():
|
||||||
|
boto3.client("s3", region_name="us-east-1").create_bucket(Bucket=BUCKET)
|
||||||
|
yield S3Storage(
|
||||||
|
bucket=BUCKET,
|
||||||
|
endpoint_url=None,
|
||||||
|
access_key="testing",
|
||||||
|
secret_key="testing",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_list_download_delete(s3_storage):
|
||||||
|
s3_storage.upload_bytes("backups/a.enc", b"alpha")
|
||||||
|
s3_storage.upload_bytes("backups/b.enc", b"beta")
|
||||||
|
s3_storage.upload_bytes("autre/c.enc", b"gamma")
|
||||||
|
|
||||||
|
assert set(s3_storage.list_keys("backups/")) == {"backups/a.enc", "backups/b.enc"}
|
||||||
|
assert s3_storage.download_bytes("backups/a.enc") == b"alpha"
|
||||||
|
|
||||||
|
s3_storage.delete("backups/a.enc")
|
||||||
|
assert s3_storage.list_keys("backups/") == ["backups/b.enc"]
|
||||||
Reference in New Issue
Block a user