# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2016, 2017 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.
"""Deposit actions."""
from __future__ import absolute_import, print_function
import json
from copy import deepcopy
from functools import partial
from flask import Blueprint, abort, current_app, make_response, request, \
url_for
from invenio_db import db
from invenio_oauth2server import require_api_auth, require_oauth_scopes
from invenio_pidstore.errors import PIDInvalidAction
from invenio_records_rest.utils import obj_or_import_string
from invenio_records_rest.views import \
create_error_handlers as records_rest_error_handlers
from invenio_records_rest.views import \
create_url_rules as records_rest_url_rules
from invenio_records_rest.views import need_record_permission, pass_record
from invenio_rest import ContentNegotiatedMethodView
from invenio_rest.views import create_api_errorhandler
from webargs import fields
from webargs.flaskparser import use_kwargs
from werkzeug.utils import secure_filename
from ..api import Deposit
from ..errors import FileAlreadyExists, WrongFile
from ..scopes import write_scope
from ..search import DepositSearch
from ..signals import post_action
from ..utils import extract_actions_from_class
[docs]def create_error_handlers(blueprint):
"""Create error handlers on blueprint."""
blueprint.errorhandler(PIDInvalidAction)(create_api_errorhandler(
status=403, message='Invalid action'
))
records_rest_error_handlers(blueprint)
[docs]def create_blueprint(endpoints):
"""Create Invenio-Deposit-REST blueprint.
See: :data:`invenio_deposit.config.DEPOSIT_REST_ENDPOINTS`.
:param endpoints: List of endpoints configuration.
:returns: The configured blueprint.
"""
blueprint = Blueprint(
'invenio_deposit_rest',
__name__,
url_prefix='',
)
create_error_handlers(blueprint)
for endpoint, options in (endpoints or {}).items():
options = deepcopy(options)
if 'files_serializers' in options:
files_serializers = options.get('files_serializers')
files_serializers = {mime: obj_or_import_string(func)
for mime, func in files_serializers.items()}
del options['files_serializers']
else:
files_serializers = {}
if 'record_serializers' in options:
serializers = options.get('record_serializers')
serializers = {mime: obj_or_import_string(func)
for mime, func in serializers.items()}
else:
serializers = {}
file_list_route = options.pop(
'file_list_route',
'{0}/files'.format(options['item_route'])
)
file_item_route = options.pop(
'file_item_route',
'{0}/files/<path:key>'.format(options['item_route'])
)
options.setdefault('search_class', DepositSearch)
search_class = obj_or_import_string(options['search_class'])
# records rest endpoints will use the deposit class as record class
options.setdefault('record_class', Deposit)
record_class = obj_or_import_string(options['record_class'])
# backward compatibility for indexer class
options.setdefault('indexer_class', None)
for rule in records_rest_url_rules(endpoint, **options):
blueprint.add_url_rule(**rule)
search_class_kwargs = {}
if options.get('search_index'):
search_class_kwargs['index'] = options['search_index']
if options.get('search_type'):
search_class_kwargs['doc_type'] = options['search_type']
ctx = dict(
read_permission_factory=obj_or_import_string(
options.get('read_permission_factory_imp')
),
create_permission_factory=obj_or_import_string(
options.get('create_permission_factory_imp')
),
update_permission_factory=obj_or_import_string(
options.get('update_permission_factory_imp')
),
delete_permission_factory=obj_or_import_string(
options.get('delete_permission_factory_imp')
),
record_class=record_class,
search_class=partial(search_class, **search_class_kwargs),
default_media_type=options.get('default_media_type'),
)
deposit_actions = DepositActionResource.as_view(
DepositActionResource.view_name.format(endpoint),
serializers=serializers,
pid_type=options['pid_type'],
ctx=ctx,
)
blueprint.add_url_rule(
'{0}/actions/<any({1}):action>'.format(
options['item_route'],
','.join(extract_actions_from_class(record_class)),
),
view_func=deposit_actions,
methods=['POST'],
)
deposit_files = DepositFilesResource.as_view(
DepositFilesResource.view_name.format(endpoint),
serializers=files_serializers,
pid_type=options['pid_type'],
ctx=ctx,
)
blueprint.add_url_rule(
file_list_route,
view_func=deposit_files,
methods=['GET', 'POST', 'PUT'],
)
deposit_file = DepositFileResource.as_view(
DepositFileResource.view_name.format(endpoint),
serializers=files_serializers,
pid_type=options['pid_type'],
ctx=ctx,
)
blueprint.add_url_rule(
file_item_route,
view_func=deposit_file,
methods=['GET', 'PUT', 'DELETE'],
)
return blueprint
[docs]class DepositActionResource(ContentNegotiatedMethodView):
"""Deposit action resource."""
view_name = '{0}_actions'
def __init__(self, serializers, pid_type, ctx, *args, **kwargs):
"""Constructor."""
super(DepositActionResource, self).__init__(
serializers,
default_media_type=ctx.get('default_media_type'),
*args,
**kwargs
)
for key, value in ctx.items():
setattr(self, key, value)
@pass_record
@need_record_permission('update_permission_factory')
[docs] def post(self, pid, record, action):
"""Handle deposit action.
After the action is executed, a
:class:`invenio_deposit.signals.post_action` signal is sent.
Permission required: `update_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
:param action: The action to execute.
"""
record = getattr(record, action)(pid=pid)
db.session.commit()
# Refresh the PID and record metadata
db.session.refresh(pid)
db.session.refresh(record.model)
post_action.send(current_app._get_current_object(), action=action,
pid=pid, deposit=record)
response = self.make_response(pid, record,
202 if action == 'publish' else 201)
endpoint = '.{0}_item'.format(pid.pid_type)
location = url_for(endpoint, pid_value=pid.pid_value, _external=True)
response.headers.extend(dict(Location=location))
return response
[docs]class DepositFilesResource(ContentNegotiatedMethodView):
"""Deposit files resource."""
view_name = '{0}_files'
def __init__(self, serializers, pid_type, ctx, *args, **kwargs):
"""Constructor."""
super(DepositFilesResource, self).__init__(
serializers,
*args,
**kwargs
)
for key, value in ctx.items():
setattr(self, key, value)
@pass_record
@need_record_permission('read_permission_factory')
[docs] def get(self, pid, record):
"""Get files.
Permission required: `read_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
:returns: The files.
"""
return self.make_response(obj=record.files, pid=pid, record=record)
@require_api_auth()
@require_oauth_scopes(write_scope.id)
@pass_record
@need_record_permission('update_permission_factory')
[docs] def post(self, pid, record):
"""Handle POST deposit files.
Permission required: `update_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
"""
# load the file
uploaded_file = request.files['file']
# file name
key = secure_filename(
request.form.get('name') or uploaded_file.filename
)
# check if already exists a file with this name
if key in record.files:
raise FileAlreadyExists()
# add it to the deposit
record.files[key] = uploaded_file.stream
record.commit()
db.session.commit()
return self.make_response(
obj=record.files[key].obj, pid=pid, record=record, status=201)
@require_api_auth()
@require_oauth_scopes(write_scope.id)
@pass_record
@need_record_permission('update_permission_factory')
[docs] def put(self, pid, record):
"""Handle the sort of the files through the PUT deposit files.
Expected input in body PUT:
.. code-block:: javascript
[
{
"id": 1
},
{
"id": 2
},
...
}
Permission required: `update_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
:returns: The files.
"""
try:
ids = [data['id'] for data in json.loads(
request.data.decode('utf-8'))]
except KeyError:
raise WrongFile()
record.files.sort_by(*ids)
record.commit()
db.session.commit()
return self.make_response(obj=record.files, pid=pid, record=record)
[docs]class DepositFileResource(ContentNegotiatedMethodView):
"""Deposit files resource."""
view_name = '{0}_file'
get_args = dict(
version_id=fields.UUID(
location='headers',
load_from='version_id',
),
)
"""GET query arguments."""
def __init__(self, serializers, pid_type, ctx, *args, **kwargs):
"""Constructor."""
super(DepositFileResource, self).__init__(
serializers,
*args,
**kwargs
)
for key, value in ctx.items():
setattr(self, key, value)
@use_kwargs(get_args)
@pass_record
@need_record_permission('read_permission_factory')
[docs] def get(self, pid, record, key, version_id, **kwargs):
"""Get file.
Permission required: `read_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
:param key: Unique identifier for the file in the deposit.
:param version_id: File version. Optional. If no version is provided,
the last version is retrieved.
:returns: the file content.
"""
try:
obj = record.files[str(key)].get_version(version_id=version_id)
return self.make_response(
obj=obj or abort(404), pid=pid, record=record)
except KeyError:
abort(404)
@require_api_auth()
@require_oauth_scopes(write_scope.id)
@pass_record
@need_record_permission('update_permission_factory')
[docs] def put(self, pid, record, key):
"""Handle the file rename through the PUT deposit file.
Permission required: `update_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
:param key: Unique identifier for the file in the deposit.
"""
try:
data = json.loads(request.data.decode('utf-8'))
new_key = data['filename']
except KeyError:
raise WrongFile()
new_key_secure = secure_filename(new_key)
if not new_key_secure or new_key != new_key_secure:
raise WrongFile()
try:
obj = record.files.rename(str(key), new_key_secure)
except KeyError:
abort(404)
record.commit()
db.session.commit()
return self.make_response(obj=obj, pid=pid, record=record)
@require_api_auth()
@require_oauth_scopes(write_scope.id)
@pass_record
@need_record_permission('update_permission_factory')
[docs] def delete(self, pid, record, key):
"""Handle DELETE deposit file.
Permission required: `update_permission_factory`.
:param pid: Pid object (from url).
:param record: Record object resolved from the pid.
:param key: Unique identifier for the file in the deposit.
"""
try:
del record.files[str(key)]
record.commit()
db.session.commit()
return make_response('', 204)
except KeyError:
abort(404, 'The specified object does not exist or has already '
'been deleted.')