Skip to content

Commit

Permalink
Add attachment support (fixes #103)
Browse files Browse the repository at this point in the history
  • Loading branch information
leplatrem committed Nov 4, 2024
1 parent 3832110 commit 88ee1dc
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 20 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ build-backend = "setuptools.build_meta"
[project.optional-dependencies]
dev = [
"kinto",
"kinto-attachment",
"ruff",
"pytest",
"pytest-asyncio",
Expand Down
47 changes: 27 additions & 20 deletions src/kinto_http/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import asyncio
import functools
import inspect
import json
import logging
import mimetypes
import os
import uuid
from collections import OrderedDict
from contextlib import contextmanager
Expand Down Expand Up @@ -862,33 +865,37 @@ def purge_history(self, *, bucket=None, safe=True, if_match=None) -> List[Dict]:
return resp["data"]

@retry_timeout
def add_attachment(self, record=None, bucket=None, collection=None):
def add_attachment(
self,
id,
filepath,
bucket=None,
collection=None,
data=None,
permissions=None,
mimetype=None,
):
with open(filepath, "rb") as f:
filecontent = f.read()
filename = os.path.basename(filepath)
filecontent = open(filepath, "rb").read()
mimetype, _ = mimetypes.guess_type(filepath)
if mimetype is None:
mimetype, _ = mimetypes.guess_type(filepath)
multipart = [("attachment", (filename, filecontent, mimetype))]

endpoint = self._get_endpoint("attachment", id=id, bucket=bucket, collection=collection, permissions=None, data=None)
resp = requests.post(
endpoint = self._get_endpoint("attachment", id=id, bucket=bucket, collection=collection)
resp, _ = self.session.request(
"post",
endpoint,
data={
"data": json.dumps(data),
"permissions": json.dumps(permissions),
},
data=json.dumps(data) if data is not None else None,
permissions=json.dumps(permissions) if permissions is not None else None,
files=multipart,
headers={"Authorization": AUTHZ}
)
resp.raise_for_status()
return resp

@retry_timeout
def remove_attachment(self, ):
endpoint = self._get_endpoint("attachment", id=id, bucket=bucket, collection=collection, permissions=None, data=None)
requests.delete(
endpoint,
headers={"Authorization": AUTHZ}
)
resp.raise_for_status()

def remove_attachment(self, id, bucket=None, collection=None):
endpoint = self._get_endpoint("attachment", id=id, bucket=bucket, collection=collection)
resp, _ = self.session.request("delete", endpoint)
return resp

def __repr__(self) -> str:
if self.collection_name:
Expand Down
1 change: 1 addition & 0 deletions src/kinto_http/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Endpoints(object):
"collection": "{root}/buckets/{bucket}/collections/{collection}",
"records": "{root}/buckets/{bucket}/collections/{collection}/records", # NOQA
"record": "{root}/buckets/{bucket}/collections/{collection}/records/{id}", # NOQA
"attachment": "{root}/buckets/{bucket}/collections/{collection}/records/{id}/attachment", # NOQA
}

def __init__(self, root=""):
Expand Down
3 changes: 3 additions & 0 deletions tests/config/kinto.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ kinto.paginate_by = 5

kinto.includes = kinto.plugins.flush
kinto.plugins.accounts
kinto_attachment

multiauth.policies = account
multiauth.policy.account.use = kinto.plugins.accounts.AccountsPolicy
kinto.account_create_principals = system.Everyone
kinto.account_write_principals = account:user
kinto.bucket_create_principals = account:user

kinto.attachment.base_path = /tmp

[server:main]
use = egg:waitress#main
host = 0.0.0.0
Expand Down
22 changes: 22 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1391,3 +1391,25 @@ def test_purging_of_history(client_setup: Client):
client.purge_history(bucket="mybucket")
url = "/buckets/mybucket/history"
client.session.request.assert_called_with("delete", url, headers=None)


def test_add_attachment_guesses_mimetype(record_setup: Client, tmp_path):
client = record_setup
mock_response(client.session)

p = tmp_path / "file.txt"
p.write_text("hello")
client.add_attachment(
id="abc",
bucket="a",
collection="b",
filepath=p,
)

client.session.request.assert_called_with(
"post",
"/buckets/a/collections/b/records/abc/attachment",
data=None,
permissions=None,
files=[("attachment", ("file.txt", b"hello", "text/plain"))],
)
45 changes: 45 additions & 0 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,48 @@ def test_replication(functional_setup):
replication.replicate(origin, destination)
records = client.get_records(bucket="destination", collection="coll")
assert len(records) == 10


def test_adding_an_attachment(functional_setup, tmp_path):
client = functional_setup
with client.batch(bucket="mozilla", collection="payments") as batch:
batch.create_bucket()
batch.create_collection()

p = tmp_path / "file.txt"
p.write_text("hello")

client.add_attachment(
id="abc",
filepath=p,
bucket="mozilla",
collection="payments",
data={"secret": "psssssst!"},
permissions={"write": ["system.Everyone"]},
mimetype="text/custom",
)

record = client.get_record(bucket="mozilla", collection="payments", id="abc")
assert "attachment" in record["data"]
assert record["data"]["attachment"]["filename"] == "file.txt"
assert record["data"]["attachment"]["mimetype"] == "text/custom"
assert "secret" in record["data"]
assert "system.Everyone" in record["permissions"]["write"]


def test_removing_an_attachment(functional_setup, tmp_path):
client = functional_setup.clone(bucket="mozilla", collection="payments")
with client.batch() as batch:
batch.create_bucket()
batch.create_collection()
p = tmp_path / "file.txt"
p.write_text("hello")
client.add_attachment(
id="abc",
filepath=p,
)

client.remove_attachment(id="abc")

record = client.get_record(id="abc")
assert record["data"]["attachment"] is None
42 changes: 42 additions & 0 deletions tests/test_functional_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,45 @@ async def test_patch_record_jsonpatch(functional_async_setup):
assert record["data"]["hello"] == "world"
assert record["data"]["goodnight"] == "moon"
assert record["permissions"]["read"] == ["alice"]


async def test_adding_an_attachment(functional_async_setup, tmp_path):
client = functional_async_setup.clone(bucket="mozilla", collection="payments")
await client.create_bucket()
await client.create_collection()

p = tmp_path / "file.txt"
p.write_text("hello")

await client.add_attachment(
id="abc",
filepath=p,
data={"secret": "psssssst!"},
permissions={"write": ["system.Everyone"]},
mimetype="text/custom",
)

record = await client.get_record(id="abc")
assert "attachment" in record["data"]
assert record["data"]["attachment"]["filename"] == "file.txt"
assert record["data"]["attachment"]["mimetype"] == "text/custom"
assert "secret" in record["data"]
assert "system.Everyone" in record["permissions"]["write"]


async def test_removing_an_attachment(functional_async_setup, tmp_path):
client = functional_async_setup.clone(bucket="mozilla", collection="payments")
await client.create_bucket()
await client.create_collection()

p = tmp_path / "file.txt"
p.write_text("hello")
await client.add_attachment(
id="abc",
filepath=p,
)

await client.remove_attachment(id="abc")

record = await client.get_record(id="abc")
assert record["data"]["attachment"] is None

0 comments on commit 88ee1dc

Please sign in to comment.