Skip to content

Commit

Permalink
views: records-ui file download view
Browse files Browse the repository at this point in the history
* Adds pluggable file download view for Records-UI.

Signed-off-by: Lars Holm Nielsen <lars.holm.nielsen@cern.ch>
  • Loading branch information
lnielsen committed Mar 29, 2016
1 parent 717a692 commit 2e5b8f9
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 31 deletions.
90 changes: 69 additions & 21 deletions invenio_files_rest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,46 @@
)


def file_download_ui(pid, record, **kwargs):
"""File download view for a given record.
Plug this method into your ``RECORDS_UI_ENDPOINTS`` configuration:
.. code-block:: python
RECORDS_UI_ENDPOINTS = dict(
recid=dict(
# ...
route='/records/<pid_value/files/<filename>',
view_imp='invenio_files_rest.views.file_download_ui',
)
)
"""
# Extract file from record.
fileobj = None
filename = request.view_args.get('filename')

for f in record.get('files', []):
if filename and filename == f.get('filename'):
fileobj = f
break

if fileobj is None:
abort(404)

bucket_id = fileobj['bucket']
key = fileobj['filename']

return ObjectResource.send_object(
bucket_id, key,
expected_chksum=fileobj.get('checksum'),
logger_data=dict(
bucket_id=bucket_id,
pid_type=pid.pid_type,
pid_value=pid.pid_value,
))


class BucketCollectionResource(ContentNegotiatedMethodView):
""""Bucket collection resource."""

Expand Down Expand Up @@ -400,6 +440,34 @@ def __init__(self, serializers=None, *args, **kwargs):
**kwargs
)

@classmethod
def send_object(cls, bucket_id, key, version_id=None, expected_chksum=None,
logger_data=None):
"""Send an object for a given bucket."""
bucket = Bucket.get(bucket_id)
if bucket is None:
abort(404, 'Bucket does not exist.')

permission = current_permission_factory(bucket, action='objects-read')

if permission is not None and not permission.can():
if current_user.is_authenticated:
abort(403, 'You do not have permissions to download the file.')
# TODO: Send user to login page. (not for REST API)
abort(401)

obj = ObjectVersion.get(bucket_id, key, version_id=version_id)
if obj is None:
abort(404, 'Object does not exist.')

# TODO: implement access control
# TODO: implement support for tokens
if expected_chksum and obj.file.checksum != expected_chksum:
current_app.logger.warning(
'File checksum mismatch detected.', extra=logger_data)

return obj.file.send_file()

@use_kwargs(get_args)
def get(self, bucket_id, key, version_id=None, **kwargs):
"""Get object.
Expand Down Expand Up @@ -437,27 +505,7 @@ def get(self, bucket_id, key, version_id=None, **kwargs):
:statuscode 403: access denied
:statuscode 404: Object does not exist
"""
# TODO: Support partial range requests.
# TODO: Support for access token
# TODO: Exception if file is not found (deleted by hand or accident)

# Retrieve bucket.
bucket = Bucket.get(bucket_id)
if bucket is None:
abort(404, 'Bucket does not exist.')

permission = current_permission_factory(bucket, action='objects-read')

if permission is not None and not permission.can():
if current_user.is_authenticated:
abort(403)
abort(401)

obj = ObjectVersion.get(bucket_id, key, version_id=version_id)
if obj is None:
abort(404, 'Object does not exist.')

return obj.file.send_file()
return self.send_object(bucket_id, key, version_id=version_id)

@use_kwargs(put_args)
def put(self, bucket_id, key, content_length=None, content_md5=None):
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'invenio-accounts>=1.0.0a2',
'invenio-admin>=1.0.0a3',
'invenio-celery>=1.0.0a4',
'invenio-records-ui>=1.0.0a5',
'isort>=4.2.2',
'mock>=1.3.0',
'pep257>=0.7.0',
Expand Down
27 changes: 17 additions & 10 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
from invenio_files_rest.views import blueprint


@pytest.yield_fixture(scope='session', autouse=True)
def app(request):
@pytest.fixture(scope='session', autouse=True)
def base_app():
"""Flask application fixture."""
app_ = Flask('testapp')
app_.config.update(
Expand All @@ -78,14 +78,21 @@ def app(request):
InvenioDB(app_)
Babel(app_)
Menu(app_)
InvenioAccounts(app_)
InvenioAccess(app_)
app_.register_blueprint(accounts_blueprint)
InvenioFilesREST(app_)
app_.register_blueprint(blueprint)

with app_.app_context():
yield app_

return app_


@pytest.yield_fixture(scope='session', autouse=True)
def app(base_app):
"""Flask application fixture."""
InvenioAccounts(base_app)
InvenioAccess(base_app)
base_app.register_blueprint(accounts_blueprint)
InvenioFilesREST(base_app)
base_app.register_blueprint(blueprint)

with base_app.app_context():
yield base_app


@pytest.yield_fixture()
Expand Down
95 changes: 95 additions & 0 deletions tests/test_views_records_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015, 2016 CERN.
#
# Invenio is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307, USA.
#
# In applying this license, CERN does not
# waive the privileges and immunities granted to it by virtue of its status
# as an Intergovernmental Organization or submit itself to any jurisdiction.


"""Records-UI custom view func tests."""

from __future__ import absolute_import, print_function

import uuid

from flask import url_for
from invenio_pidstore import InvenioPIDStore
from invenio_pidstore.models import PersistentIdentifier, PIDStatus
from invenio_records import InvenioRecords
from invenio_records.api import Record
from invenio_records_ui import InvenioRecordsUI


def test_file_download_ui(base_app, objects, db):
"""Test get buckets."""
app = base_app
app.config.update(dict(
RECORDS_UI_DEFAULT_PERMISSION_FACTORY=None, # No permission checking
RECORDS_UI_ENDPOINTS=dict(
recid=dict(
pid_type='recid',
route='/records/<pid_value>',
),
recid_files=dict(
pid_type='recid',
route='/records/<pid_value>/files/<filename>',
view_imp='invenio_files_rest.views.file_download_ui',
),
)
))
InvenioRecords(app)
InvenioPIDStore(app)
InvenioRecordsUI(app)

obj1 = objects[0]

with app.app_context():
# Record 1 - Live record
rec_uuid = uuid.uuid4()
PersistentIdentifier.create(
'recid', '1', object_type='rec', object_uuid=rec_uuid,
status=PIDStatus.REGISTERED)
Record.create({
'title': 'Registered',
'recid': 1,
'files': [
{'filename': obj1.key, 'bucket': str(obj1.bucket_id),
'checksum': 'invalid'},
]
}, id_=rec_uuid)
db.session.commit()

main_url = url_for('invenio_records_ui.recid', pid_value='1')
file_url = url_for(
'invenio_records_ui.recid_files', pid_value='1', filename=obj1.key)
no_file_url = url_for(
'invenio_records_ui.recid_files', pid_value='1', filename='')
invalid_file_url = url_for(
'invenio_records_ui.recid_files', pid_value='1', filename='no')

with app.test_client() as client:
res = client.get(main_url)
assert res.status_code == 200
res = client.get(file_url)
assert res.status_code == 200
res = client.get(no_file_url)
assert res.status_code == 404
res = client.get(invalid_file_url)
assert res.status_code == 404

0 comments on commit 2e5b8f9

Please sign in to comment.