Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

views: records-ui file download view #39

Merged
merged 1 commit into from
Mar 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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