feat: add MinioClient wrapper for list/get/put/delete object operations
This commit is contained in:
parent
2a7a197b95
commit
f4126ff3ec
45
backend/app/minio_client.py
Normal file
45
backend/app/minio_client.py
Normal file
@ -0,0 +1,45 @@
|
||||
import io
|
||||
from minio.error import S3Error
|
||||
|
||||
|
||||
class MinioClient:
|
||||
def __init__(self, client, bucket: str):
|
||||
self._client = client
|
||||
self._bucket = bucket
|
||||
|
||||
def list_objects(self, prefix: str) -> list[dict]:
|
||||
objects = self._client.list_objects(self._bucket, prefix=prefix, recursive=False)
|
||||
result = []
|
||||
for obj in objects:
|
||||
result.append({
|
||||
"key": obj.object_name,
|
||||
"size": obj.size,
|
||||
"is_dir": obj.object_name.endswith("/"),
|
||||
"modifiedAt": obj.last_modified.isoformat() if obj.last_modified else None,
|
||||
})
|
||||
return result
|
||||
|
||||
def get_object(self, key: str) -> bytes:
|
||||
try:
|
||||
response = self._client.get_object(self._bucket, key)
|
||||
except S3Error as exc:
|
||||
if exc.code == "NoSuchKey":
|
||||
raise FileNotFoundError(key) from exc
|
||||
raise
|
||||
try:
|
||||
return response.read()
|
||||
finally:
|
||||
response.close()
|
||||
response.release_conn()
|
||||
|
||||
def put_object(self, key: str, data: bytes, content_type: str) -> None:
|
||||
self._client.put_object(
|
||||
self._bucket,
|
||||
key,
|
||||
io.BytesIO(data),
|
||||
length=len(data),
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
def delete_object(self, key: str) -> None:
|
||||
self._client.remove_object(self._bucket, key)
|
||||
108
backend/tests/test_minio_client.py
Normal file
108
backend/tests/test_minio_client.py
Normal file
@ -0,0 +1,108 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from minio.error import S3Error
|
||||
|
||||
from app.minio_client import MinioClient
|
||||
|
||||
|
||||
class _FakeObject:
|
||||
def __init__(self, object_name, size, last_modified):
|
||||
self.object_name = object_name
|
||||
self.size = size
|
||||
self.last_modified = last_modified
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, data: bytes):
|
||||
self._data = data
|
||||
self.closed = False
|
||||
self.released = False
|
||||
|
||||
def read(self):
|
||||
return self._data
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def release_conn(self):
|
||||
self.released = True
|
||||
|
||||
|
||||
class FakeMinioSDK:
|
||||
def __init__(self):
|
||||
self.objects = {}
|
||||
self.put_calls = []
|
||||
self.removed_keys = []
|
||||
|
||||
def list_objects(self, bucket, prefix="", recursive=False):
|
||||
for key, (data, content_type, last_modified) in self.objects.items():
|
||||
if key.startswith(prefix):
|
||||
yield _FakeObject(key, len(data), last_modified)
|
||||
|
||||
def get_object(self, bucket, key):
|
||||
if key not in self.objects:
|
||||
raise S3Error("NoSuchKey", "Object does not exist", key, "req-1", "host-1", None)
|
||||
data, _, _ = self.objects[key]
|
||||
return _FakeResponse(data)
|
||||
|
||||
def put_object(self, bucket, key, stream, length, content_type):
|
||||
data = stream.read()
|
||||
self.objects[key] = (data, content_type, datetime.now(timezone.utc))
|
||||
self.put_calls.append((bucket, key, content_type))
|
||||
|
||||
def remove_object(self, bucket, key):
|
||||
del self.objects[key]
|
||||
self.removed_keys.append(key)
|
||||
|
||||
|
||||
def test_list_objects_returns_keys_matching_prefix():
|
||||
sdk = FakeMinioSDK()
|
||||
sdk.objects["产品文档/架构设计.md"] = (b"# hi", "text/markdown", datetime(2026, 8, 1, tzinfo=timezone.utc))
|
||||
sdk.objects["团队规范/编码规范.md"] = (b"# rules", "text/markdown", datetime(2026, 8, 1, tzinfo=timezone.utc))
|
||||
client = MinioClient(sdk, bucket="dochub")
|
||||
|
||||
result = client.list_objects("产品文档/")
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["key"] == "产品文档/架构设计.md"
|
||||
assert result[0]["size"] == 4
|
||||
|
||||
|
||||
def test_get_object_returns_bytes():
|
||||
sdk = FakeMinioSDK()
|
||||
sdk.objects["产品文档/架构设计.md"] = (b"# architecture", "text/markdown", datetime.now(timezone.utc))
|
||||
client = MinioClient(sdk, bucket="dochub")
|
||||
|
||||
data = client.get_object("产品文档/架构设计.md")
|
||||
|
||||
assert data == b"# architecture"
|
||||
|
||||
|
||||
def test_get_object_raises_file_not_found_for_missing_key():
|
||||
sdk = FakeMinioSDK()
|
||||
client = MinioClient(sdk, bucket="dochub")
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
client.get_object("不存在/文档.md")
|
||||
|
||||
|
||||
def test_put_object_stores_data_with_content_type():
|
||||
sdk = FakeMinioSDK()
|
||||
client = MinioClient(sdk, bucket="dochub")
|
||||
|
||||
client.put_object("产品文档/新文档.md", b"# new doc", "text/markdown")
|
||||
|
||||
assert sdk.objects["产品文档/新文档.md"][0] == b"# new doc"
|
||||
assert sdk.put_calls == [("dochub", "产品文档/新文档.md", "text/markdown")]
|
||||
|
||||
|
||||
def test_delete_object_removes_key():
|
||||
sdk = FakeMinioSDK()
|
||||
sdk.objects["产品文档/旧文档.md"] = (b"old", "text/markdown", datetime.now(timezone.utc))
|
||||
client = MinioClient(sdk, bucket="dochub")
|
||||
|
||||
client.delete_object("产品文档/旧文档.md")
|
||||
|
||||
assert "产品文档/旧文档.md" not in sdk.objects
|
||||
assert sdk.removed_keys == ["产品文档/旧文档.md"]
|
||||
Loading…
Reference in New Issue
Block a user