Compare commits
No commits in common. "e2ec32554042608a0fab04fd90cf9b2f3dda5d2e" and "ec41382f9d0cb21c1f7148142bc495a6a5ddd592" have entirely different histories.
e2ec325540
...
ec41382f9d
@ -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"
|
||||||
|
@ -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 ###
|
|
@ -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"
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
|
||||||
|
@ -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"):
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user