Some security fixes and other fixes for file storage.
Also added unittests for creating files.
This commit is contained in:
parent
fd5bcaec52
commit
7c04e01cde
@ -8,12 +8,14 @@ import eve.utils
|
|||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
from eve.methods.patch import patch_internal
|
from eve.methods.patch import patch_internal
|
||||||
from eve.methods.put import put_internal
|
from eve.methods.put import put_internal
|
||||||
from flask import Blueprint
|
from flask import Blueprint, safe_join
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask import request
|
from flask import request
|
||||||
|
from flask import abort
|
||||||
from flask import send_from_directory
|
from flask import send_from_directory
|
||||||
from flask import url_for, helpers
|
from flask import url_for, helpers
|
||||||
|
|
||||||
|
from application import utils
|
||||||
from application.utils import remove_private_keys
|
from application.utils import remove_private_keys
|
||||||
from application.utils.cdn import hash_file_path
|
from application.utils.cdn import hash_file_path
|
||||||
from application.utils.encoding import Encoder
|
from application.utils.encoding import Encoder
|
||||||
@ -75,7 +77,7 @@ def build_thumbnails(file_path=None, file_id=None):
|
|||||||
file_ = files_collection.find_one({"_id": ObjectId(file_id)})
|
file_ = files_collection.find_one({"_id": ObjectId(file_id)})
|
||||||
file_path = file_['name']
|
file_path = file_['name']
|
||||||
|
|
||||||
file_full_path = os.path.join(app.config['SHARED_DIR'], file_path[:2],
|
file_full_path = safe_join(safe_join(app.config['SHARED_DIR'], file_path[:2]),
|
||||||
file_path)
|
file_path)
|
||||||
# Does the original file exist?
|
# Does the original file exist?
|
||||||
if not os.path.isfile(file_full_path):
|
if not os.path.isfile(file_full_path):
|
||||||
@ -147,26 +149,29 @@ def index(file_name=None):
|
|||||||
return jsonify({'url': url_for('file_storage.index', file_name=file_name)})
|
return jsonify({'url': url_for('file_storage.index', file_name=file_name)})
|
||||||
|
|
||||||
|
|
||||||
def process_file(src_file):
|
def process_file(file_id, src_file):
|
||||||
"""Process the file
|
"""Process the file.
|
||||||
|
|
||||||
|
:param file_id: '_id' key of the file
|
||||||
|
:param src_file: POSTed data of the file, lacks private properties.
|
||||||
"""
|
"""
|
||||||
from application import app
|
from application import app
|
||||||
|
|
||||||
file_id = src_file['_id']
|
src_file = utils.remove_private_keys(src_file)
|
||||||
# Remove properties that do not belong in the collection
|
|
||||||
internal_fields = ['_id', '_etag', '_updated', '_created', '_status']
|
|
||||||
for field in internal_fields:
|
|
||||||
src_file.pop(field, None)
|
|
||||||
|
|
||||||
files_collection = app.data.driver.db['files']
|
filename = src_file['name']
|
||||||
file_abs_path = os.path.join(
|
file_abs_path = safe_join(safe_join(app.config['SHARED_DIR'], filename[:2]), filename)
|
||||||
app.config['SHARED_DIR'], src_file['name'][:2], src_file['name'])
|
|
||||||
|
if not os.path.exists(file_abs_path):
|
||||||
|
log.warning("POSTed file document %r refers to non-existant file on file system %s!",
|
||||||
|
file_id, file_abs_path)
|
||||||
|
abort(422, "POSTed file document refers to non-existant file on file system!")
|
||||||
|
|
||||||
src_file['length'] = os.stat(file_abs_path).st_size
|
src_file['length'] = os.stat(file_abs_path).st_size
|
||||||
content_type = src_file['content_type'].split('/')
|
content_type = src_file['content_type'].split('/')
|
||||||
src_file['format'] = content_type[1]
|
src_file['format'] = content_type[1]
|
||||||
mime_type = content_type[0]
|
mime_type = content_type[0]
|
||||||
src_file['file_path'] = src_file['name']
|
src_file['file_path'] = filename
|
||||||
|
|
||||||
if mime_type == 'image':
|
if mime_type == 'image':
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@ -197,7 +202,7 @@ def process_file(src_file):
|
|||||||
src_file['variations'] = []
|
src_file['variations'] = []
|
||||||
# Create variations
|
# Create variations
|
||||||
for v in variations:
|
for v in variations:
|
||||||
root, ext = os.path.splitext(src_file['name'])
|
root, ext = os.path.splitext(filename)
|
||||||
filename = "{0}-{1}p.{2}".format(root, res_y, v)
|
filename = "{0}-{1}p.{2}".format(root, res_y, v)
|
||||||
video_duration = None
|
video_duration = None
|
||||||
if src_video_data['duration']:
|
if src_video_data['duration']:
|
||||||
@ -250,6 +255,9 @@ def process_file(src_file):
|
|||||||
|
|
||||||
p = Process(target=encode, args=(file_abs_path, src_file, res_y))
|
p = Process(target=encode, args=(file_abs_path, src_file, res_y))
|
||||||
p.start()
|
p.start()
|
||||||
|
else:
|
||||||
|
log.info("POSTed file was of type %r, which isn't thumbnailed/encoded.", mime_type)
|
||||||
|
|
||||||
if mime_type != 'video':
|
if mime_type != 'video':
|
||||||
# Sync the whole subdir
|
# Sync the whole subdir
|
||||||
sync_path = os.path.split(file_abs_path)[0]
|
sync_path = os.path.split(file_abs_path)[0]
|
||||||
@ -259,7 +267,7 @@ def process_file(src_file):
|
|||||||
p.start()
|
p.start()
|
||||||
|
|
||||||
# Update the original file with additional info, e.g. image resolution
|
# Update the original file with additional info, e.g. image resolution
|
||||||
r = put_internal('files', src_file, **{'_id': ObjectId(file_id)})
|
put_internal('files', src_file, _id=ObjectId(file_id))
|
||||||
|
|
||||||
|
|
||||||
def delete_file(file_item):
|
def delete_file(file_item):
|
||||||
@ -383,7 +391,14 @@ def post_POST_files(request, payload):
|
|||||||
"""After an file object has been created, we do the necessary processing
|
"""After an file object has been created, we do the necessary processing
|
||||||
and further update it.
|
and further update it.
|
||||||
"""
|
"""
|
||||||
process_file(request.get_json())
|
|
||||||
|
if 200 <= payload.status_code < 300:
|
||||||
|
import json
|
||||||
|
posted_properties = json.loads(request.data)
|
||||||
|
private_properties = json.loads(payload.data)
|
||||||
|
file_id = private_properties['_id']
|
||||||
|
|
||||||
|
process_file(file_id, posted_properties)
|
||||||
|
|
||||||
|
|
||||||
def before_deleting_file(item):
|
def before_deleting_file(item):
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import bson
|
||||||
|
from eve import RFC1123_DATE_FORMAT
|
||||||
|
|
||||||
|
|
||||||
def remove_private_keys(document):
|
def remove_private_keys(document):
|
||||||
@ -11,3 +16,19 @@ def remove_private_keys(document):
|
|||||||
del patch_info[key]
|
del patch_info[key]
|
||||||
|
|
||||||
return patch_info
|
return patch_info
|
||||||
|
|
||||||
|
|
||||||
|
class PillarJSONEncoder(json.JSONEncoder):
|
||||||
|
"""JSON encoder with support for Pillar resources."""
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
if obj.tzinfo is None:
|
||||||
|
raise ValueError('All datetime.datetime objects should be timezone-aware.')
|
||||||
|
return obj.strftime(RFC1123_DATE_FORMAT)
|
||||||
|
|
||||||
|
if isinstance(obj, bson.ObjectId):
|
||||||
|
return str(obj)
|
||||||
|
|
||||||
|
# Let the base class default method raise the TypeError
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
@ -70,7 +70,7 @@ def push_to_storage(project_id, full_path, backend='cgs'):
|
|||||||
# XXX Make public on the fly if it's an image and small preview.
|
# XXX Make public on the fly if it's an image and small preview.
|
||||||
# This should happen by reading the database (push to storage
|
# This should happen by reading the database (push to storage
|
||||||
# should change to accomodate it).
|
# should change to accomodate it).
|
||||||
if full_path.endswith('-t.jpg'):
|
if blob is not None and full_path.endswith('-t.jpg'):
|
||||||
blob.make_public()
|
blob.make_public()
|
||||||
os.remove(full_path)
|
os.remove(full_path)
|
||||||
|
|
||||||
|
BIN
tests/BlenderDesktopLogo.png
Normal file
BIN
tests/BlenderDesktopLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@ -34,6 +34,7 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
app.config['BLENDER_ID_ENDPOINT'] = BLENDER_ID_ENDPOINT
|
app.config['BLENDER_ID_ENDPOINT'] = BLENDER_ID_ENDPOINT
|
||||||
logging.getLogger('application').setLevel(logging.DEBUG)
|
logging.getLogger('application').setLevel(logging.DEBUG)
|
||||||
logging.getLogger('werkzeug').setLevel(logging.DEBUG)
|
logging.getLogger('werkzeug').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('eve').setLevel(logging.DEBUG)
|
||||||
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.client = app.test_client()
|
self.client = app.test_client()
|
||||||
@ -59,7 +60,20 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
projects_collection.insert_one(EXAMPLE_PROJECT)
|
projects_collection.insert_one(EXAMPLE_PROJECT)
|
||||||
result = files_collection.insert_one(file)
|
result = files_collection.insert_one(file)
|
||||||
file_id = result.inserted_id
|
file_id = result.inserted_id
|
||||||
return file_id, EXAMPLE_FILE
|
return file_id, file
|
||||||
|
|
||||||
|
def ensure_project_exists(self, project_overrides=None):
|
||||||
|
with self.app.test_request_context():
|
||||||
|
projects_collection = self.app.data.driver.db['projects']
|
||||||
|
assert isinstance(projects_collection, pymongo.collection.Collection)
|
||||||
|
|
||||||
|
project = copy.deepcopy(EXAMPLE_PROJECT)
|
||||||
|
if project_overrides is not None:
|
||||||
|
project.update(project_overrides)
|
||||||
|
|
||||||
|
result = projects_collection.insert_one(project)
|
||||||
|
project_id = result.inserted_id
|
||||||
|
return project_id, project
|
||||||
|
|
||||||
def htp_blenderid_validate_unhappy(self):
|
def htp_blenderid_validate_unhappy(self):
|
||||||
"""Sets up HTTPretty to mock unhappy validation flow."""
|
"""Sets up HTTPretty to mock unhappy validation flow."""
|
||||||
|
58
tests/test_file_storage.py
Normal file
58
tests/test_file_storage.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Test cases for file handling."""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
|
from common_test_class import AbstractPillarTest, MY_PATH
|
||||||
|
from common_test_data import EXAMPLE_FILE
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadingTest(AbstractPillarTest):
|
||||||
|
|
||||||
|
def test_create_file_missing_on_fs(self):
|
||||||
|
from application import utils
|
||||||
|
from application.utils import PillarJSONEncoder
|
||||||
|
|
||||||
|
to_post = utils.remove_private_keys(EXAMPLE_FILE)
|
||||||
|
json_file = json.dumps(to_post, cls=PillarJSONEncoder)
|
||||||
|
|
||||||
|
with self.app.test_request_context():
|
||||||
|
self.ensure_project_exists()
|
||||||
|
|
||||||
|
resp = self.client.post('/files',
|
||||||
|
data=json_file,
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
self.assertEqual(422, resp.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_file_exists_on_fs(self):
|
||||||
|
from application import utils
|
||||||
|
from application.utils import PillarJSONEncoder
|
||||||
|
|
||||||
|
filename = 'BlenderDesktopLogo.png'
|
||||||
|
full_file = copy.deepcopy(EXAMPLE_FILE)
|
||||||
|
full_file[u'name'] = filename
|
||||||
|
to_post = utils.remove_private_keys(full_file)
|
||||||
|
json_file = json.dumps(to_post, cls=PillarJSONEncoder)
|
||||||
|
|
||||||
|
with self.app.test_request_context():
|
||||||
|
self.ensure_project_exists()
|
||||||
|
|
||||||
|
target_dir = os.path.join(self.app.config['SHARED_DIR'], filename[:2])
|
||||||
|
if os.path.exists(target_dir):
|
||||||
|
assert os.path.isdir(target_dir)
|
||||||
|
else:
|
||||||
|
os.makedirs(target_dir)
|
||||||
|
shutil.copy(os.path.join(MY_PATH, filename), target_dir)
|
||||||
|
|
||||||
|
resp = self.client.post('/files',
|
||||||
|
data=json_file,
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
self.assertEqual(201, resp.status_code)
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user