Compare commits

..

No commits in common. "e2ec32554042608a0fab04fd90cf9b2f3dda5d2e" and "ec41382f9d0cb21c1f7148142bc495a6a5ddd592" have entirely different histories.

9 changed files with 128 additions and 393 deletions

View File

@ -4,6 +4,8 @@
set -gx BASENAME localhost:5000 set -gx BASENAME localhost:5000
function sachet_init_db -d "initialize db" function sachet_init_db -d "initialize db"
flask --debug --app sachet.server db drop --yes
flask --debug --app sachet.server db create
flask --debug --app sachet.server user create --username admin --admin yes --password password123 flask --debug --app sachet.server user create --username admin --admin yes --password password123
flask --debug --app sachet.server user create --username user --admin no --password password123 flask --debug --app sachet.server user create --username user --admin no --password password123
end end
@ -29,18 +31,13 @@ end
function sachet_upload -d "uploads a file" function sachet_upload -d "uploads a file"
argparse 's/session=?' -- $argv argparse 's/session=?' -- $argv
set FNAME (basename $argv) set FNAME (basename $argv)
set URL (http --session=$_flag_session post $BASENAME/files file_name=$FNAME | tee /dev/tty | jq -r .url) set URL (http --session=$_flag_session post $BASENAME/files file_name=$FNAME | jq -r .url)
http --session=$_flag_session -f post $BASENAME/$URL/content \ http --session=$_flag_session -f post $BASENAME/$URL/content upload@$argv
upload@$argv \
dzuuid=(cat /dev/urandom | xxd -ps | head -c 32) \
dzchunkindex=0 \
dztotalchunks=1
end end
function sachet_upload_meme -d "uploads a random meme" function sachet_upload_meme -d "uploads a random meme"
argparse 's/session=?' -- $argv
set MEME ~/med/memes/woof/(ls ~/med/memes/woof | shuf | head -n 1) set MEME ~/med/memes/woof/(ls ~/med/memes/woof | shuf | head -n 1)
sachet_upload -s$_flag_session $MEME sachet_upload $MEME
end end
function sachet_list -d "lists files on a given page" function sachet_list -d "lists files on a given page"

View File

@ -1,72 +0,0 @@
"""empty message
Revision ID: 70ab3c81827a
Revises: 4cd7cdbc2d1f
Create Date: 2023-05-07 21:52:08.250195
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
# revision identifiers, used by Alembic.
revision = "70ab3c81827a"
down_revision = "4cd7cdbc2d1f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"uploads",
sa.Column("upload_id", sa.String(), nullable=False),
sa.Column("share_id", sqlalchemy_utils.types.uuid.UUIDType(), nullable=True),
sa.Column("create_date", sa.DateTime(), nullable=False),
sa.Column("total_chunks", sa.Integer(), nullable=False),
sa.Column("recv_chunks", sa.Integer(), nullable=False),
sa.Column("completed", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["share_id"],
["shares.share_id"],
),
sa.PrimaryKeyConstraint("upload_id"),
)
op.create_table(
"chunks",
sa.Column("chunk_id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("create_date", sa.DateTime(), nullable=False),
sa.Column("index", sa.Integer(), nullable=False),
sa.Column("upload_id", sa.String(), nullable=True),
sa.Column("filename", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["upload_id"],
["uploads.upload_id"],
),
sa.PrimaryKeyConstraint("chunk_id"),
)
with op.batch_alter_table("shares", schema=None) as batch_op:
batch_op.alter_column(
"share_id",
existing_type=sa.NUMERIC(precision=16),
type_=sqlalchemy_utils.types.uuid.UUIDType(),
existing_nullable=False,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("shares", schema=None) as batch_op:
batch_op.alter_column(
"share_id",
existing_type=sqlalchemy_utils.types.uuid.UUIDType(),
type_=sa.NUMERIC(precision=16),
existing_nullable=False,
)
op.drop_table("chunks")
op.drop_table("uploads")
# ### end Alembic commands ###

View File

@ -21,6 +21,7 @@ class TestingConfig(BaseConfig):
class DevelopmentConfig(BaseConfig): class DevelopmentConfig(BaseConfig):
SERVER_NAME = "localhost.dev"
SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_dev" + ".db" SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_dev" + ".db"
BCRYPT_LOG_ROUNDS = 4 BCRYPT_LOG_ROUNDS = 4
SACHET_FILE_DIR = "storage_dev" SACHET_FILE_DIR = "storage_dev"

View File

@ -2,7 +2,7 @@ import uuid
import io import io
from flask import Blueprint, request, jsonify, send_file from flask import Blueprint, request, jsonify, send_file
from flask.views import MethodView from flask.views import MethodView
from sachet.server.models import Share, Permissions, Upload, Chunk from sachet.server.models import Share, Permissions
from sachet.server.views_common import ModelAPI, ModelListAPI, auth_required from sachet.server.views_common import ModelAPI, ModelListAPI, auth_required
from sachet.server import storage, db from sachet.server import storage, db
@ -68,55 +68,7 @@ files_blueprint.add_url_rule(
) )
class FileContentAPI(MethodView): class FileContentAPI(ModelAPI):
def recv_upload(self, share):
"""Receive chunked uploads.
share : Share
Share we are uploading to.
"""
chunk_file = request.files.get("upload")
if not chunk_file:
return (
jsonify(dict(status="fail", message="Missing chunk data in request.")),
400,
)
chunk_data = chunk_file.read()
try:
dz_uuid = request.form["dzuuid"]
dz_chunk_index = int(request.form["dzchunkindex"])
dz_total_chunks = int(request.form["dztotalchunks"])
except KeyError as err:
return (
jsonify(
dict(status="fail", message=f"Missing data for chunking; {err}")
),
400,
)
except ValueError as err:
return (
jsonify(dict(status="fail", message=f"{err}")),
400,
)
chunk = Chunk(dz_chunk_index, dz_uuid, dz_total_chunks, share, chunk_data)
db.session.add(chunk)
db.session.commit()
upload = chunk.upload
upload.recv_chunks = upload.recv_chunks + 1
if upload.recv_chunks >= upload.total_chunks:
upload.complete()
if upload.completed:
share.initialized = True
db.session.delete(upload)
db.session.commit()
return jsonify(dict(status="success", message="Upload completed.")), 201
else:
return jsonify(dict(status="success", message="Chunk uploaded.")), 200
@auth_required(required_permissions=(Permissions.CREATE,), allow_anonymous=True) @auth_required(required_permissions=(Permissions.CREATE,), allow_anonymous=True)
def post(self, share_id, auth_user=None): def post(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=uuid.UUID(share_id)).first() share = Share.query.filter_by(share_id=uuid.UUID(share_id)).first()
@ -148,7 +100,20 @@ class FileContentAPI(MethodView):
423, 423,
) )
return self.recv_upload(share) 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,), allow_anonymous=True) @auth_required(required_permissions=(Permissions.MODIFY,), allow_anonymous=True)
def put(self, share_id, auth_user=None): def put(self, share_id, auth_user=None):
@ -185,7 +150,17 @@ class FileContentAPI(MethodView):
423, 423,
) )
return self.recv_upload(share) 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,), allow_anonymous=True) @auth_required(required_permissions=(Permissions.READ,), allow_anonymous=True)
def get(self, share_id, auth_user=None): def get(self, share_id, auth_user=None):

View File

@ -6,7 +6,6 @@ from bitmask import Bitmask
from marshmallow import fields, ValidationError from marshmallow import fields, ValidationError
from flask import request, jsonify, url_for, current_app from flask import request, jsonify, url_for, current_app
from sqlalchemy_utils import UUIDType from sqlalchemy_utils import UUIDType
from sqlalchemy import event
import uuid import uuid
@ -282,148 +281,3 @@ class Share(db.Model):
def get_handle(self): def get_handle(self):
return storage.get_file(str(self.share_id)) return storage.get_file(str(self.share_id))
@classmethod
def __declare_last__(cls):
@event.listens_for(cls, "before_delete")
def share_before_delete(mapper, connection, share):
file = share.get_handle()
file.delete()
class Upload(db.Model):
"""Upload instance for a given file.
Parameters
----------
upload_id : str
ID associated to this upload.
total_chunks: int
Total amount of chunks in this upload.
share_id : uuid.UUID
Assigns this upload to the given share id.
Attributes
----------
upload_id : str
ID associated to this upload.
total_chunks : int
Total amount of chunks in this upload.
recv_chunks : int
Amount of chunks received in this upload.
completed : bool
Whether the file has been fully uploaded.
share : Share
The share this upload is for.
chunks : list of Chunk
Chunks composing this upload.
create_date : DateTime
Time this upload was started.
"""
__tablename__ = "uploads"
upload_id = db.Column(db.String, primary_key=True)
share_id = db.Column(UUIDType(), db.ForeignKey("shares.share_id"))
share = db.relationship("Share", backref=db.backref("upload"))
create_date = db.Column(db.DateTime, nullable=False)
total_chunks = db.Column(db.Integer, nullable=False)
recv_chunks = db.Column(db.Integer, nullable=False, default=0)
completed = db.Column(db.Boolean, nullable=False, default=False)
chunks = db.relationship(
"Chunk",
backref=db.backref("upload"),
order_by="Chunk.chunk_id",
cascade="all, delete",
)
def __init__(self, upload_id, total_chunks, share_id):
self.share = Share.query.filter_by(share_id=share_id).first()
if self.share is None:
raise KeyError(f"Share '{self.share_id}' could not be found.")
self.upload_id = upload_id
self.total_chunks = total_chunks
self.create_date = datetime.datetime.now()
def complete(self):
"""Merge chunks, save the file, then clean up."""
tmp_file = storage.get_file(f"{self.share.share_id}_{self.upload_id}")
with tmp_file.open(mode="ab") as tmp_f:
for chunk in self.chunks:
chunk_file = storage.get_file(chunk.filename)
with chunk_file.open(mode="rb") as chunk_f:
data = chunk_f.read()
tmp_f.write(data)
# replace the old file
old_file = self.share.get_handle()
old_file.delete()
tmp_file.rename(str(self.share.share_id))
self.completed = True
class Chunk(db.Model):
"""Single chunk within an upload.
Parameters
----------
index : int
Index of this chunk within an upload.
upload_id : str
ID of the upload this chunk is associated to.
total_chunks : int
Total amount of chunks within this upload.
share : Share
Assigns this chunk to the given share.
data : bytes
Raw chunk data.
Attributes
----------
chunk_id : int
ID unique for all chunks (not just in a single upload.)
create_date : DateTime
Time this chunk was received.
index : int
Index of this chunk within an upload.
upload : Upload
Upload this chunk is associated to.
filename : str
Filename the data is stored in.
"""
__tablename__ = "chunks"
chunk_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
create_date = db.Column(db.DateTime, nullable=False)
index = db.Column(db.Integer, nullable=False)
upload_id = db.Column(db.String, db.ForeignKey("uploads.upload_id"))
filename = db.Column(db.String, nullable=False)
def __init__(self, index, upload_id, total_chunks, share, data):
self.upload = Upload.query.filter_by(upload_id=upload_id).first()
if self.upload is None:
self.upload = Upload(upload_id, total_chunks, share.share_id)
self.upload.recv_chunks = 0
db.session.add(self.upload)
self.upload_id = upload_id
self.create_date = datetime.datetime.now()
self.index = index
self.filename = f"{share.share_id}_{self.upload_id}_{self.index}"
file = storage.get_file(self.filename)
with file.open(mode="wb") as f:
f.write(data)
@classmethod
def __declare_last__(cls):
@event.listens_for(cls, "before_delete")
def chunk_before_delete(mapper, connection, chunk):
file = storage.get_file(chunk.filename)
file.delete()

View File

@ -43,7 +43,7 @@ class FileSystem(Storage):
self._path = self._storage._get_path(name) self._path = self._storage._get_path(name)
self._path.touch() self._path.touch()
def delete(self): def delete(self, name):
self._path.unlink() self._path.unlink()
def open(self, mode="r"): def open(self, mode="r"):

View File

@ -1,12 +1,8 @@
import pytest import pytest
import uuid
from math import ceil
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
from sachet.server.models import Permissions, User from sachet.server.models import Permissions, User
from werkzeug.datastructures import FileStorage
from io import BytesIO
from bitmask import Bitmask from bitmask import Bitmask
from pathlib import Path from pathlib import Path
import random import random
@ -204,58 +200,3 @@ def auth(tokens):
return ret return ret
return auth_headers return auth_headers
@pytest.fixture
def upload(client):
"""Perform chunked upload of some data.
Parameters
----------
url : str
URL to upload to.
data : BytesIO
Stream of data to upload.
You can use BytesIO(data) to convert raw bytes to a stream.
headers : dict, optional
Headers to upload with.
chunk_size : int, optional
Size of chunks in bytes.
method : function
Method like client.post or client.put to use.
"""
def upload(url, data, headers={}, chunk_size=int(2e6), method=client.post):
data_size = len(data.getbuffer())
buf = data.getbuffer()
upload_uuid = uuid.uuid4()
total_chunks = int(ceil(data_size / chunk_size))
resp = None
for chunk_idx in range(total_chunks):
start = chunk_size * chunk_idx
end = min(chunk_size * (chunk_idx + 1), data_size)
resp = method(
url,
headers=headers,
data={
"upload": FileStorage(
stream=BytesIO(buf[start:end]), filename="upload"
),
"dzuuid": str(upload_uuid),
"dzchunkindex": chunk_idx,
"dztotalchunks": total_chunks,
},
content_type="multipart/form-data",
)
if not resp.status_code == 200 or resp.status_code == 201:
break
return resp
return upload

View File

@ -6,7 +6,7 @@ import uuid
"""Test anonymous authentication to endpoints.""" """Test anonymous authentication to endpoints."""
def test_files(client, auth, rand, upload): def test_files(client, auth, rand):
# set create perm for anon users # set create perm for anon users
resp = client.patch( resp = client.patch(
"/admin/settings", "/admin/settings",
@ -28,9 +28,10 @@ def test_files(client, auth, rand, upload):
upload_data = rand.randbytes(4000) upload_data = rand.randbytes(4000)
# upload file to share # upload file to share
resp = upload( resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data), data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
) )
assert resp.status_code == 201 assert resp.status_code == 201
@ -59,12 +60,12 @@ def test_files(client, auth, rand, upload):
# modify share # modify share
upload_data = rand.randbytes(4000) upload_data = rand.randbytes(4000)
resp = upload( resp = client.put(
url + "/content", url + "/content",
BytesIO(upload_data), data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
method=client.put, content_type="multipart/form-data",
) )
assert resp.status_code == 201 assert resp.status_code == 200
resp = client.patch( resp = client.patch(
url, url,
json={"file_name": "new_bin.bin"}, json={"file_name": "new_bin.bin"},
@ -129,7 +130,7 @@ def test_files(client, auth, rand, upload):
assert resp.status_code == 404 assert resp.status_code == 404
def test_files_invalid(client, auth, rand, upload): def test_files_invalid(client, auth, rand):
# set create perm for anon users # set create perm for anon users
resp = client.patch( resp = client.patch(
"/admin/settings", "/admin/settings",
@ -150,7 +151,11 @@ def test_files_invalid(client, auth, rand, upload):
data = resp.get_json() data = resp.get_json()
url = data.get("url") url = data.get("url")
upload_data = rand.randbytes(4000) upload_data = rand.randbytes(4000)
resp = upload(url + "/content", BytesIO(upload_data)) resp = client.post(
url + "/content",
data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
)
assert resp.status_code == 201 assert resp.status_code == 201
# disable all permissions # disable all permissions
@ -162,15 +167,28 @@ def test_files_invalid(client, auth, rand, upload):
assert resp.status_code == 200 assert resp.status_code == 200
# test initializing a share without perms # test initializing a share without perms
resp = upload(url + "/content", BytesIO(upload_data)) resp = client.post(
uninit_url + "/content",
data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
)
assert resp.status_code == 401 assert resp.status_code == 401
# test reading a share without perms # test reading a share without perms
resp = client.get(url + "/content") resp = client.get(url + "/content")
# test modifying an uninitialized share without perms # test modifying an uninitialized share without perms
resp = upload(uninit_url + "/content", BytesIO(upload_data), method=client.put) resp = client.put(
uninit_url + "/content",
data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
)
assert resp.status_code == 401
assert resp.status_code == 401 assert resp.status_code == 401
# test modifying a share without perms # test modifying a share without perms
resp = upload(url + "/content", BytesIO(upload_data), method=client.put) resp = client.put(
url + "/content",
data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
)
assert resp.status_code == 401 assert resp.status_code == 401
# test deleting a share without perms # test deleting a share without perms

View File

@ -1,8 +1,6 @@
import pytest import pytest
from os.path import basename
from io import BytesIO from io import BytesIO
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from sachet.server import storage
import uuid import uuid
"""Test file share endpoints.""" """Test file share endpoints."""
@ -12,7 +10,7 @@ import uuid
# this might be redundant because test_storage tests the backends already # this might be redundant because test_storage tests the backends already
@pytest.mark.parametrize("client", [{"SACHET_STORAGE": "filesystem"}], indirect=True) @pytest.mark.parametrize("client", [{"SACHET_STORAGE": "filesystem"}], indirect=True)
class TestSuite: class TestSuite:
def test_sharing(self, client, users, auth, rand, upload): def test_sharing(self, client, users, auth, rand):
# create share # create share
resp = client.post( resp = client.post(
"/files", headers=auth("jeff"), json={"file_name": "content.bin"} "/files", headers=auth("jeff"), json={"file_name": "content.bin"}
@ -27,11 +25,14 @@ class TestSuite:
upload_data = rand.randbytes(4000) upload_data = rand.randbytes(4000)
resp = upload( # upload file to share
resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
chunk_size=1230, data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 201 assert resp.status_code == 201
@ -57,10 +58,7 @@ class TestSuite:
) )
assert resp.status_code == 404 assert resp.status_code == 404
for f in storage.list_files(): def test_modification(self, client, users, auth, rand):
assert basename(url) not in f.name
def test_modification(self, client, users, auth, rand, upload):
# create share # create share
resp = client.post( resp = client.post(
"/files", headers=auth("jeff"), json={"file_name": "content.bin"} "/files", headers=auth("jeff"), json={"file_name": "content.bin"}
@ -72,12 +70,14 @@ class TestSuite:
new_data = rand.randbytes(4000) new_data = rand.randbytes(4000)
# upload file to share # upload file to share
resp = upload( resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 201
# modify metadata # modify metadata
resp = client.patch( resp = client.patch(
@ -88,13 +88,12 @@ class TestSuite:
assert resp.status_code == 200 assert resp.status_code == 200
# modify file contents # modify file contents
resp = upload( resp = client.put(
url + "/content", url + "/content",
BytesIO(new_data),
headers=auth("jeff"), headers=auth("jeff"),
method=client.put, data={"upload": FileStorage(stream=BytesIO(new_data), filename="upload")},
) )
assert resp.status_code == 201 assert resp.status_code == 200
# read file # read file
resp = client.get( resp = client.get(
@ -104,7 +103,7 @@ class TestSuite:
assert resp.data == new_data assert resp.data == new_data
assert "filename=new_bin.bin" in resp.headers["Content-Disposition"].split("; ") assert "filename=new_bin.bin" in resp.headers["Content-Disposition"].split("; ")
def test_invalid(self, client, users, auth, rand, upload): def test_invalid(self, client, users, auth, rand):
"""Test invalid requests.""" """Test invalid requests."""
upload_data = rand.randbytes(4000) upload_data = rand.randbytes(4000)
@ -142,51 +141,66 @@ class TestSuite:
url = data.get("url") url = data.get("url")
# test invalid methods # test invalid methods
resp = upload( resp = client.put(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
method=client.put, data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 423 assert resp.status_code == 423
resp = upload( resp = client.patch(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
method=client.patch, data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 405 assert resp.status_code == 405
# test other user being unable to upload to this share # test other user being unable to upload to this share
resp = upload( resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("dave"), headers=auth("dave"),
data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 403 assert resp.status_code == 403
# upload file to share (properly) # upload file to share (properly)
resp = upload( resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 201 assert resp.status_code == 201
# test other user being unable to modify this share # test other user being unable to modify this share
resp = upload( resp = client.put(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("dave"), headers=auth("dave"),
method=client.put, data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 403 assert resp.status_code == 403
# test not allowing re-upload # test not allowing re-upload
resp = upload( resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 423 assert resp.status_code == 423
@ -196,7 +210,7 @@ class TestSuite:
resp = client.get(url + "/content", headers=auth("no_read_user")) resp = client.get(url + "/content", headers=auth("no_read_user"))
assert resp.status_code == 403 assert resp.status_code == 403
def test_locking(self, client, users, auth, rand, upload): def test_locking(self, client, users, auth, rand):
# upload share # upload share
resp = client.post( resp = client.post(
"/files", headers=auth("jeff"), json={"file_name": "content.bin"} "/files", headers=auth("jeff"), json={"file_name": "content.bin"}
@ -204,10 +218,13 @@ class TestSuite:
data = resp.get_json() data = resp.get_json()
url = data.get("url") url = data.get("url")
upload_data = rand.randbytes(4000) upload_data = rand.randbytes(4000)
resp = upload( resp = client.post(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 201 assert resp.status_code == 201
@ -219,11 +236,13 @@ class TestSuite:
assert resp.status_code == 200 assert resp.status_code == 200
# attempt to modify share # attempt to modify share
resp = upload( resp = client.put(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
method=client.put, data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 423 assert resp.status_code == 423
@ -242,13 +261,15 @@ class TestSuite:
assert resp.status_code == 200 assert resp.status_code == 200
# attempt to modify share # attempt to modify share
resp = upload( resp = client.put(
url + "/content", url + "/content",
BytesIO(upload_data),
headers=auth("jeff"), headers=auth("jeff"),
method=client.put, data={
"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")
},
content_type="multipart/form-data",
) )
assert resp.status_code == 201 assert resp.status_code == 200
# attempt to delete share # attempt to delete share
resp = client.delete( resp = client.delete(