/files/: implemented endpoint
This commit is contained in:
parent
2fba574773
commit
af21887402
@ -31,6 +31,7 @@ pytest-env==0.8.1
|
|||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
SQLAlchemy==2.0.5.post1
|
SQLAlchemy==2.0.5.post1
|
||||||
|
SQLAlchemy-Utils==0.40.0
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
typing_extensions==4.5.0
|
typing_extensions==4.5.0
|
||||||
Werkzeug==2.2.3
|
Werkzeug==2.2.3
|
||||||
|
@ -42,5 +42,9 @@ from sachet.server.admin.views import admin_blueprint
|
|||||||
|
|
||||||
app.register_blueprint(admin_blueprint)
|
app.register_blueprint(admin_blueprint)
|
||||||
|
|
||||||
|
from sachet.server.files.views import files_blueprint
|
||||||
|
|
||||||
|
app.register_blueprint(files_blueprint)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import jwt
|
import uuid
|
||||||
from flask import Blueprint, request, jsonify
|
import io
|
||||||
|
from flask import Blueprint, request, jsonify, send_file
|
||||||
from flask.views import MethodView
|
from flask.views import MethodView
|
||||||
from sachet.server.models import File, Permissions
|
from sachet.server.models import Share, Permissions
|
||||||
from sachet.server.views_common import ModelAPI, auth_required
|
from sachet.server.views_common import ModelAPI, auth_required
|
||||||
|
from sachet.server import storage, db
|
||||||
|
|
||||||
files_blueprint = Blueprint("files_blueprint", __name__)
|
files_blueprint = Blueprint("files_blueprint", __name__)
|
||||||
|
|
||||||
@ -10,26 +12,156 @@ files_blueprint = Blueprint("files_blueprint", __name__)
|
|||||||
class FilesAPI(ModelAPI):
|
class FilesAPI(ModelAPI):
|
||||||
"""Files metadata API."""
|
"""Files metadata API."""
|
||||||
|
|
||||||
@auth_required
|
@auth_required(required_permissions=(Permissions.READ,))
|
||||||
def get(self, id, auth_user=None):
|
def get(self, share_id, auth_user=None):
|
||||||
pass
|
share = Share.query.filter_by(share_id=share_id).first()
|
||||||
|
return super().get(share)
|
||||||
|
|
||||||
|
@auth_required(required_permissions=(Permissions.MODIFY,))
|
||||||
|
def patch(self, share_id, auth_user=None):
|
||||||
|
share = Share.query.filter_by(share_id=share_id).first()
|
||||||
|
return super().patch(share)
|
||||||
|
|
||||||
|
@auth_required(required_permissions=(Permissions.MODIFY,))
|
||||||
|
def put(self, share_id, auth_user=None):
|
||||||
|
share = Share.query.filter_by(share_id=share_id).first()
|
||||||
|
return super().put(share)
|
||||||
|
|
||||||
|
@auth_required(required_permissions=(Permissions.DELETE,))
|
||||||
|
def delete(self, share_id, auth_user=None):
|
||||||
|
share = Share.query.filter_by(share_id=share_id).first()
|
||||||
|
return super().delete(share)
|
||||||
|
|
||||||
|
|
||||||
files_blueprint.add_url_rule(
|
files_blueprint.add_url_rule(
|
||||||
"/files/<id>",
|
"/files/<share_id>",
|
||||||
view_func=FilesAPI.as_view("files_api"),
|
view_func=FilesAPI.as_view("files_api"),
|
||||||
methods=["POST", "PUT", "PATCH", "GET", "DELETE"],
|
methods=["PUT", "PATCH", "GET", "DELETE"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FileCreationAPI(ModelAPI):
|
||||||
|
@auth_required(required_permissions=(Permissions.CREATE,))
|
||||||
|
def post(self, auth_user=None):
|
||||||
|
# silent means it will return None if there is no JSON
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
data["owner_name"] = auth_user.username
|
||||||
|
return super().post(Share, data)
|
||||||
|
|
||||||
|
|
||||||
files_blueprint.add_url_rule(
|
files_blueprint.add_url_rule(
|
||||||
"/files/<id>/content",
|
"/files",
|
||||||
view_func=FilesContentAPI.as_view("files_content_api"),
|
view_func=FileCreationAPI.as_view("files_creation_api"),
|
||||||
methods=["PUT", "GET"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
users_blueprint.add_url_rule(
|
class FileContentAPI(ModelAPI):
|
||||||
"/users/<username>",
|
@auth_required(required_permissions=(Permissions.CREATE,))
|
||||||
view_func=UserAPI.as_view("user_api"),
|
def post(self, share_id, auth_user=None):
|
||||||
methods=["GET", "PATCH", "PUT"],
|
share = Share.query.filter_by(share_id=uuid.UUID(share_id)).first()
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
return (
|
||||||
|
jsonify({"status": "fail", "message": "This share does not exist."})
|
||||||
|
), 404
|
||||||
|
|
||||||
|
if auth_user != share.owner:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"message": "Share must be initialized by its owner.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
|
if share.initialized:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"message": "Share already initialized. Use PUT to modify the share.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
423,
|
||||||
|
)
|
||||||
|
|
||||||
|
upload = request.files["upload"]
|
||||||
|
data = upload.read()
|
||||||
|
file = share.get_handle()
|
||||||
|
with file.open(mode="wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
share.initialized = True
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (
|
||||||
|
jsonify({"status": "success", "message": "Share has been initialized."}),
|
||||||
|
201,
|
||||||
|
)
|
||||||
|
|
||||||
|
@auth_required(required_permissions=(Permissions.MODIFY,))
|
||||||
|
def put(self, share_id, auth_user=None):
|
||||||
|
share = Share.query.filter_by(share_id=share_id).first()
|
||||||
|
if not share:
|
||||||
|
return (
|
||||||
|
jsonify({"status": "fail", "message": "This share does not exist."})
|
||||||
|
), 404
|
||||||
|
|
||||||
|
if not share.initialized:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"message": "Share not initialized. Use POST to upload for the first time to this share.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
423,
|
||||||
|
)
|
||||||
|
|
||||||
|
upload = request.files["upload"]
|
||||||
|
data = upload.read()
|
||||||
|
file = share.get_handle()
|
||||||
|
|
||||||
|
with file.open(mode="wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
return (
|
||||||
|
jsonify({"status": "success", "message": "Share has been modified."}),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
@auth_required(required_permissions=(Permissions.READ,))
|
||||||
|
def get(self, share_id, auth_user=None):
|
||||||
|
share = Share.query.filter_by(share_id=share_id).first()
|
||||||
|
if not share:
|
||||||
|
return (
|
||||||
|
jsonify({"status": "fail", "message": "This share does not exist."})
|
||||||
|
), 404
|
||||||
|
|
||||||
|
if not share.initialized:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"message": "Share not initialized. Use POST to upload for the first time to this share.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
file = share.get_handle()
|
||||||
|
with file.open(mode="rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
return send_file(io.BytesIO(data), download_name=str(share.share_id))
|
||||||
|
|
||||||
|
|
||||||
|
files_blueprint.add_url_rule(
|
||||||
|
"/files/<share_id>/content",
|
||||||
|
view_func=FileContentAPI.as_view("files_content_api"),
|
||||||
|
methods=["POST", "PUT", "GET"],
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from sachet.server import app, db, ma, bcrypt
|
from sachet.server import app, db, ma, bcrypt, storage
|
||||||
from marshmallow import fields, ValidationError
|
|
||||||
from flask import request, jsonify
|
|
||||||
from functools import wraps
|
|
||||||
import datetime
|
import datetime
|
||||||
import jwt
|
import jwt
|
||||||
from bitmask import Bitmask
|
|
||||||
from enum import IntFlag
|
from enum import IntFlag
|
||||||
|
from bitmask import Bitmask
|
||||||
|
from marshmallow import fields, ValidationError
|
||||||
|
from flask import request, jsonify, url_for
|
||||||
|
from sqlalchemy_utils import UUIDType
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class Permissions(IntFlag):
|
class Permissions(IntFlag):
|
||||||
@ -15,6 +16,7 @@ class Permissions(IntFlag):
|
|||||||
LOCK = 1 << 3
|
LOCK = 1 << 3
|
||||||
LIST = 1 << 4
|
LIST = 1 << 4
|
||||||
ADMIN = 1 << 5
|
ADMIN = 1 << 5
|
||||||
|
READ = 1 << 6
|
||||||
|
|
||||||
|
|
||||||
class PermissionField(fields.Field):
|
class PermissionField(fields.Field):
|
||||||
@ -188,3 +190,63 @@ class ServerSettings(db.Model):
|
|||||||
default_permissions = PermissionField()
|
default_permissions = PermissionField()
|
||||||
|
|
||||||
return Schema()
|
return Schema()
|
||||||
|
|
||||||
|
|
||||||
|
class Share(db.Model):
|
||||||
|
"""Share for a single file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
owner : User
|
||||||
|
Assign this share to this user.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
share_id : uuid.uuid4
|
||||||
|
Unique identifier for this given share.
|
||||||
|
owner : User
|
||||||
|
The user who owns this share.
|
||||||
|
initialized : bool
|
||||||
|
Since only the metadata is uploaded first, this switches to True when
|
||||||
|
the real data is uploaded.
|
||||||
|
create_date : DateTime
|
||||||
|
Time the share was created (not initialized.)
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
get_handle():
|
||||||
|
Obtain a sachet.storage.Storage.File handle. This can be used to modify
|
||||||
|
the file contents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "shares"
|
||||||
|
|
||||||
|
share_id = db.Column(UUIDType(), primary_key=True, default=uuid.uuid4)
|
||||||
|
|
||||||
|
owner_name = db.Column(db.String, db.ForeignKey("users.username"))
|
||||||
|
owner = db.relationship("User", backref=db.backref("owner"))
|
||||||
|
|
||||||
|
initialized = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
create_date = db.Column(db.DateTime, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, owner_name):
|
||||||
|
self.owner = User.query.filter_by(username=owner_name).first()
|
||||||
|
self.owner_name = self.owner.username
|
||||||
|
self.share_id = uuid.uuid4()
|
||||||
|
self.url = url_for("files_blueprint.files_api", share_id=self.share_id)
|
||||||
|
self.create_date = datetime.datetime.now()
|
||||||
|
|
||||||
|
def get_schema(self):
|
||||||
|
class Schema(ma.SQLAlchemySchema):
|
||||||
|
class Meta:
|
||||||
|
model = self
|
||||||
|
|
||||||
|
share_id = ma.auto_field(dump_only=True)
|
||||||
|
owner_name = ma.auto_field()
|
||||||
|
initialized = ma.auto_field(dump_only=True)
|
||||||
|
|
||||||
|
return Schema()
|
||||||
|
|
||||||
|
def get_handle(self):
|
||||||
|
return storage.get_file(str(self.share_id))
|
||||||
|
@ -166,17 +166,25 @@ class ModelAPI(MethodView):
|
|||||||
}
|
}
|
||||||
return jsonify(resp), 404
|
return jsonify(resp), 404
|
||||||
|
|
||||||
model.delete()
|
db.session.delete(model)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"status": "success"})
|
return jsonify({"status": "success"})
|
||||||
|
|
||||||
def post(self, ModelClass, data):
|
def post(self, ModelClass, data={}):
|
||||||
model_schema = ModelClass.get_schema()
|
"""Create new instance of a class.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ModelClass
|
||||||
|
Class to make an instance of.
|
||||||
|
data : dict
|
||||||
|
Object that can be loaded with Marshmallow to create the class.
|
||||||
|
"""
|
||||||
|
model_schema = ModelClass.get_schema(ModelClass)
|
||||||
|
|
||||||
post_json = request.get_json()
|
|
||||||
try:
|
try:
|
||||||
deserialized = model_schema.load(post_json)
|
deserialized = model_schema.load(data)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
resp = {"status": "fail", "message": f"Invalid data: {str(e)}"}
|
resp = {"status": "fail", "message": f"Invalid data: {str(e)}"}
|
||||||
return jsonify(resp), 400
|
return jsonify(resp), 400
|
||||||
@ -184,4 +192,7 @@ class ModelAPI(MethodView):
|
|||||||
# create new ModelClass instance with all the parameters given in the request
|
# create new ModelClass instance with all the parameters given in the request
|
||||||
model = ModelClass(**deserialized)
|
model = ModelClass(**deserialized)
|
||||||
|
|
||||||
return jsonify({"status": "success"}), 201
|
db.session.add(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"status": "success", "url": model.url}), 201
|
||||||
|
@ -33,14 +33,12 @@ class Storage:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class File:
|
class File:
|
||||||
"""Handle for a file and its metadata.
|
"""Handle for a file.
|
||||||
|
|
||||||
Do not instantiate this; use `Storage.get_file()`.
|
Do not instantiate this; use `Storage.get_file()`.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
metadata : _Metadata
|
|
||||||
All the metadata, accessible via dot attribute notation.
|
|
||||||
name : str
|
name : str
|
||||||
Filename
|
Filename
|
||||||
"""
|
"""
|
||||||
@ -63,7 +61,7 @@ class Storage:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete file and associated metadata."""
|
"""Delete file."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -16,10 +16,8 @@ class FileSystem(Storage):
|
|||||||
self._directory = Path(app.instance_path) / config_path
|
self._directory = Path(app.instance_path) / config_path
|
||||||
|
|
||||||
self._files_directory = self._directory / Path("files")
|
self._files_directory = self._directory / Path("files")
|
||||||
self._meta_directory = self._directory / Path("meta")
|
|
||||||
|
|
||||||
self._files_directory.mkdir(mode=0o700, exist_ok=True, parents=True)
|
self._files_directory.mkdir(mode=0o700, exist_ok=True, parents=True)
|
||||||
self._meta_directory.mkdir(mode=0o700, exist_ok=True, parents=True)
|
|
||||||
|
|
||||||
if not self._directory.is_dir():
|
if not self._directory.is_dir():
|
||||||
raise OSError(f"'{app.config['SACHET_FILE_DIR']}' is not a directory.")
|
raise OSError(f"'{app.config['SACHET_FILE_DIR']}' is not a directory.")
|
||||||
@ -28,10 +26,6 @@ class FileSystem(Storage):
|
|||||||
name = secure_filename(name)
|
name = secure_filename(name)
|
||||||
return self._files_directory / Path(name)
|
return self._files_directory / Path(name)
|
||||||
|
|
||||||
def _get_meta_path(self, name):
|
|
||||||
name = secure_filename(name)
|
|
||||||
return self._meta_directory / Path(name)
|
|
||||||
|
|
||||||
def list_files(self):
|
def list_files(self):
|
||||||
return [
|
return [
|
||||||
self.get_file(x.name)
|
self.get_file(x.name)
|
||||||
@ -47,14 +41,10 @@ class FileSystem(Storage):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self._storage = storage
|
self._storage = storage
|
||||||
self._path = self._storage._get_path(name)
|
self._path = self._storage._get_path(name)
|
||||||
self._meta_path = self._storage._get_meta_path(name)
|
|
||||||
self._path.touch()
|
self._path.touch()
|
||||||
self._meta_path.touch()
|
|
||||||
self.metadata = self._Metadata(self)
|
|
||||||
|
|
||||||
def delete(self, name):
|
def delete(self, name):
|
||||||
self._path.unlink()
|
self._path.unlink()
|
||||||
self._meta_path.unlink()
|
|
||||||
|
|
||||||
def open(self, mode="r"):
|
def open(self, mode="r"):
|
||||||
return self._path.open(mode=mode)
|
return self._path.open(mode=mode)
|
||||||
@ -64,33 +54,4 @@ class FileSystem(Storage):
|
|||||||
if new_path.exists():
|
if new_path.exists():
|
||||||
raise OSError(f"Path {path} already exists.")
|
raise OSError(f"Path {path} already exists.")
|
||||||
|
|
||||||
new_meta_path = self._storage._get_meta_path(new_name)
|
|
||||||
if new_meta_path.exists():
|
|
||||||
raise OSError(f"Path {path} already exists.")
|
|
||||||
|
|
||||||
self._path.rename(new_path)
|
self._path.rename(new_path)
|
||||||
self._meta_path.rename(new_meta_path)
|
|
||||||
|
|
||||||
class _Metadata:
|
|
||||||
def __init__(self, file):
|
|
||||||
self.__dict__["_file"] = file
|
|
||||||
|
|
||||||
@property
|
|
||||||
def __data(self):
|
|
||||||
with self._file._meta_path.open() as meta_file:
|
|
||||||
content = meta_file.read()
|
|
||||||
if len(content.strip()) == 0:
|
|
||||||
return {}
|
|
||||||
return json.loads(content)
|
|
||||||
|
|
||||||
# there is no setter for __data because it would cause __setattr__ to infinitely recurse
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
data = self.__data
|
|
||||||
data[name] = value
|
|
||||||
with self._file._meta_path.open("w") as meta_file:
|
|
||||||
content = json.dumps(data)
|
|
||||||
meta_file.write(content)
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return self.__data.get(name, None)
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import yaml
|
|
||||||
from sachet.server.users import manage
|
from sachet.server.users import manage
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from sachet.server import app, db, storage
|
from sachet.server import app, db, storage
|
||||||
@ -7,7 +6,6 @@ from sachet.server.models import Permissions, User
|
|||||||
from bitmask import Bitmask
|
from bitmask import Bitmask
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import random
|
import random
|
||||||
import itertools
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -23,10 +21,7 @@ def rand():
|
|||||||
|
|
||||||
def clear_filesystem():
|
def clear_filesystem():
|
||||||
if app.config["SACHET_STORAGE"] == "filesystem":
|
if app.config["SACHET_STORAGE"] == "filesystem":
|
||||||
for file in itertools.chain(
|
for file in storage._files_directory.iterdir():
|
||||||
storage._meta_directory.iterdir(),
|
|
||||||
storage._files_directory.iterdir(),
|
|
||||||
):
|
|
||||||
if file.is_relative_to(Path(app.instance_path)) and file.is_file():
|
if file.is_relative_to(Path(app.instance_path)) and file.is_file():
|
||||||
file.unlink()
|
file.unlink()
|
||||||
else:
|
else:
|
||||||
@ -67,7 +62,12 @@ def users(client):
|
|||||||
Returns a dictionary with all the info for each user.
|
Returns a dictionary with all the info for each user.
|
||||||
"""
|
"""
|
||||||
userinfo = dict(
|
userinfo = dict(
|
||||||
jeff=dict(password="1234", permissions=Bitmask()),
|
jeff=dict(
|
||||||
|
password="1234",
|
||||||
|
permissions=Bitmask(
|
||||||
|
Permissions.CREATE, Permissions.READ, Permissions.DELETE
|
||||||
|
),
|
||||||
|
),
|
||||||
administrator=dict(password="4321", permissions=Bitmask(Permissions.ADMIN)),
|
administrator=dict(password="4321", permissions=Bitmask(Permissions.ADMIN)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
68
tests/test_files.py
Normal file
68
tests/test_files.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import pytest
|
||||||
|
from io import BytesIO
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
|
"""Test file share endpoints."""
|
||||||
|
|
||||||
|
|
||||||
|
# if other storage backends are implemented we test them with the same suite
|
||||||
|
# this might be redundant because test_storage tests the backends already
|
||||||
|
@pytest.mark.parametrize("client", [{"SACHET_STORAGE": "filesystem"}], indirect=True)
|
||||||
|
class TestSuite:
|
||||||
|
def test_sharing(self, client, users, tokens, rand):
|
||||||
|
# create share
|
||||||
|
resp = client.post(
|
||||||
|
"/files", headers={"Authorization": f"bearer {tokens['jeff']}"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
data = resp.get_json()
|
||||||
|
url = data.get("url")
|
||||||
|
|
||||||
|
assert url is not None
|
||||||
|
assert "/files/" in url
|
||||||
|
|
||||||
|
upload_data = rand.randbytes(4000)
|
||||||
|
|
||||||
|
# upload file to share
|
||||||
|
resp = client.post(
|
||||||
|
url + "/content",
|
||||||
|
headers={"Authorization": f"bearer {tokens['jeff']}"},
|
||||||
|
data={
|
||||||
|
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
|
||||||
|
},
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
# test not allowing re-upload
|
||||||
|
resp = client.post(
|
||||||
|
url + "/content",
|
||||||
|
headers={"Authorization": f"bearer {tokens['jeff']}"},
|
||||||
|
data={
|
||||||
|
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
|
||||||
|
},
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
assert resp.status_code == 423
|
||||||
|
|
||||||
|
# read file
|
||||||
|
resp = client.get(
|
||||||
|
url + "/content",
|
||||||
|
headers={"Authorization": f"bearer {tokens['jeff']}"},
|
||||||
|
)
|
||||||
|
assert resp.data == upload_data
|
||||||
|
|
||||||
|
# test deletion
|
||||||
|
resp = client.delete(
|
||||||
|
url,
|
||||||
|
headers={"Authorization": f"bearer {tokens['jeff']}"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# file shouldn't exist anymore
|
||||||
|
resp = client.get(
|
||||||
|
url + "/content",
|
||||||
|
headers={"Authorization": f"bearer {tokens['jeff']}"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 404
|
@ -10,18 +10,12 @@ from uuid import UUID
|
|||||||
@pytest.mark.parametrize("client", [{"SACHET_STORAGE": "filesystem"}], indirect=True)
|
@pytest.mark.parametrize("client", [{"SACHET_STORAGE": "filesystem"}], indirect=True)
|
||||||
class TestSuite:
|
class TestSuite:
|
||||||
def test_creation(self, client, rand):
|
def test_creation(self, client, rand):
|
||||||
"""Test the process of creating, writing, then reading files with metadata, and also listing files."""
|
"""Test the process of creating, writing, then reading files, and also listing files."""
|
||||||
|
|
||||||
files = [
|
files = [
|
||||||
dict(
|
dict(
|
||||||
name=str(UUID(bytes=rand.randbytes(16))),
|
name=str(UUID(bytes=rand.randbytes(16))),
|
||||||
data=rand.randbytes(4000),
|
data=rand.randbytes(4000),
|
||||||
metadata=dict(
|
|
||||||
sdljkf=dict(abc="def", aaa="bbb"),
|
|
||||||
lkdsjf=dict(ld="sdlfj", sdljf="sdlkjf"),
|
|
||||||
sdlfkj="sjdlkfsldk",
|
|
||||||
ssjdklf=rand.randint(-1000, 1000),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
for i in range(25)
|
for i in range(25)
|
||||||
]
|
]
|
||||||
@ -30,15 +24,12 @@ class TestSuite:
|
|||||||
handle = storage.get_file(file["name"])
|
handle = storage.get_file(file["name"])
|
||||||
with handle.open(mode="wb") as f:
|
with handle.open(mode="wb") as f:
|
||||||
f.write(file["data"])
|
f.write(file["data"])
|
||||||
handle.metadata.test_data = file["metadata"]
|
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
handle = storage.get_file(file["name"])
|
handle = storage.get_file(file["name"])
|
||||||
with handle.open(mode="rb") as f:
|
with handle.open(mode="rb") as f:
|
||||||
saved_data = f.read()
|
saved_data = f.read()
|
||||||
assert saved_data == file["data"]
|
assert saved_data == file["data"]
|
||||||
saved_meta = handle.metadata.test_data
|
|
||||||
assert saved_meta == file["metadata"]
|
|
||||||
|
|
||||||
assert sorted([f.name for f in storage.list_files()]) == sorted(
|
assert sorted([f.name for f in storage.list_files()]) == sorted(
|
||||||
[f["name"] for f in files]
|
[f["name"] for f in files]
|
||||||
@ -50,12 +41,6 @@ class TestSuite:
|
|||||||
name=str(UUID(bytes=rand.randbytes(16))),
|
name=str(UUID(bytes=rand.randbytes(16))),
|
||||||
new_name=str(UUID(bytes=rand.randbytes(16))),
|
new_name=str(UUID(bytes=rand.randbytes(16))),
|
||||||
data=rand.randbytes(4000),
|
data=rand.randbytes(4000),
|
||||||
metadata=dict(
|
|
||||||
sdljkf=dict(abc="def", aaa="bbb"),
|
|
||||||
lkdsjf=dict(ld="sdlfj", sdljf="sdlkjf"),
|
|
||||||
sdlfkj="sjdlkfsldk",
|
|
||||||
ssjdklf=rand.randint(-1000, 1000),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
for i in range(25)
|
for i in range(25)
|
||||||
]
|
]
|
||||||
@ -64,7 +49,6 @@ class TestSuite:
|
|||||||
handle = storage.get_file(file["name"])
|
handle = storage.get_file(file["name"])
|
||||||
with handle.open(mode="wb") as f:
|
with handle.open(mode="wb") as f:
|
||||||
f.write(file["data"])
|
f.write(file["data"])
|
||||||
handle.metadata.test_data = file["metadata"]
|
|
||||||
handle.rename(file["new_name"])
|
handle.rename(file["new_name"])
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -72,5 +56,3 @@ class TestSuite:
|
|||||||
with handle.open(mode="rb") as f:
|
with handle.open(mode="rb") as f:
|
||||||
saved_data = f.read()
|
saved_data = f.read()
|
||||||
assert saved_data == file["data"]
|
assert saved_data == file["data"]
|
||||||
saved_meta = handle.metadata.test_data
|
|
||||||
assert saved_meta == file["metadata"]
|
|
||||||
|
Loading…
Reference in New Issue
Block a user