Skip to content

Commit

Permalink
global: enable Flask-IIIF extension
Browse files Browse the repository at this point in the history
* NEW Uses Flask-IIIF extension providing various image manipulation
  capabilities.

* NEW Adds possibility to refer to documents and legacy BibDocFiles
  via special path such as `/api/multimedia/image/recid:{recid}` or
  `/api/multimedia/image/recid:{recid}-{filename}` or
  `/api/multimedia/image/uuid` with proper permission checking.

* (closes #3080)

Reviewed-by: Jiri Kuncar <[email protected]>
Signed-off-by: Harris Tzovanakis <[email protected]>
  • Loading branch information
drjova committed May 28, 2015
1 parent c7c9b3e commit 213b6f1
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 0 deletions.
1 change: 1 addition & 0 deletions invenio/base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
'invenio.ext.jasmine', # after assets
'flask_breadcrumbs:Breadcrumbs',
'invenio.modules.deposit.url_converters',
'invenio.ext.iiif',
]

PACKAGES = [
Expand Down
73 changes: 73 additions & 0 deletions invenio/ext/iiif/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 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.

"""Flask-IIIF extension.
.. py:data:: IIIF_IMAGE_OPENER
There are two ways to initialize an IIIF image object, by ``fullpath`` and
``bytestream``. Can be an ``import string`` or a ``callback function``.
By default ``identifier_to_path`` return the ``fullpath`` of
:class:`~invenio.modules.documents.api:Document`.
default: :func:`~invenio.modules.documents.utis.identifier_to_path`
"""

from flask_iiif import IIIF
from flask_iiif.errors import MultimediaError

from six import string_types

from werkzeug.utils import import_string

from .utils import api_file_permission_check

__all__ = ('setup_app', )

iiif = IIIF()


def setup_app(app):
"""Setup Flask-IIIF extension."""
if 'invenio.modules.documents' in app.config.get('PACKAGES_EXCLUDE'):
raise MultimediaError(
"Could not initialize the Flask-IIIF extension because "
":class:`~invenio.modules.documents.api:Document` is missing"
)

iiif.init_app(app)
iiif.init_restful(app.extensions['restful'])
app.config.setdefault(
'IIIF_IMAGE_OPENER',
'invenio.modules.documents.utils:identifier_to_path'
)

uuid_to_source_handler = app.config['IIIF_IMAGE_OPENER']

uuid_to_source = (
import_string(uuid_to_source_handler) if
isinstance(uuid_to_source_handler, string_types) else
uuid_to_source_handler
)
iiif.uuid_to_image_opener_handler(uuid_to_source)
app.config['IIIF_CACHE_HANDLER'] = 'invenio.ext.cache:cache'

# protect the api
iiif.api_decorator_handler(api_file_permission_check)
return app
31 changes: 31 additions & 0 deletions invenio/ext/iiif/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 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.

"""Flask-IIIF extension utils."""

from invenio.modules.documents.utils import (
identifier_to_path_and_permissions
)

__all__ = ('api_file_permission_check', )


def api_file_permission_check(*args, **kwargs):
"""IIIF API file permission check."""
identifier_to_path_and_permissions(kwargs.get('uuid', ''))
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 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.

"""Test for document and legacy bibdocs access restrictions."""

import os
import shutil
import tempfile

from invenio.testsuite import InvenioTestCase, make_test_suite, run_test_suite


class DocumentAndLegacyRestrictionsTest(InvenioTestCase):

"""Test document access restrictions."""

def setUp(self):
"""Run before the test."""
from invenio.modules.documents.api import Document
self.document = Document.create({'title': 'J.A.R.V.I.S'})
self.path = tempfile.mkdtemp()

def tearDown(self):
"""Run after the tests."""
self.document = None
shutil.rmtree(self.path)

def test_legacy_syntax(self):
"""Test legacy syntax."""
from invenio.modules.documents.utils import _parse_legacy_syntax
uuid_1 = 'recid:22'
uuid_2 = 'recid:22-filename.jpg'

check_uuid_1 = _parse_legacy_syntax(uuid_1)
check_uuid_2 = _parse_legacy_syntax(uuid_2)
answer_uuid_1 = '22', None
answer_uuid_2 = '22', 'filename.jpg'

self.assertEqual(
check_uuid_1,
answer_uuid_1
)
self.assertEqual(
check_uuid_2,
answer_uuid_2
)

def test_not_found_error(self):
"""Test when the file doesn't exists."""
from werkzeug.exceptions import NotFound
from invenio.modules.documents.utils import identifier_to_path
self.assertRaises(
NotFound,
identifier_to_path,
'this_is_not_a_uuid'
)
self.assertRaises(
NotFound,
identifier_to_path,
self.document.get('uuid')
)

def test_forbidden_error(self):
"""Test when the file is restricted."""
from werkzeug.exceptions import Forbidden
from invenio.modules.documents.utils import (
identifier_to_path_and_permissions
)
content = 'S.H.I.E.L.D.'
source, sourcepath = tempfile.mkstemp()

with open(sourcepath, 'w+') as f:
f.write(content)

uri = os.path.join(self.path, 'classified.txt')
self.document.setcontents(sourcepath, uri)
self.document['restriction']['email'] = '[email protected]'
test_document = self.document.update()
self.assertRaises(
Forbidden,
identifier_to_path_and_permissions,
test_document.get('uuid')
)
shutil.rmtree(sourcepath, ignore_errors=True)

TEST_SUITE = make_test_suite(DocumentAndLegacyRestrictionsTest,)

if __name__ == "__main__":
run_test_suite(TEST_SUITE)
128 changes: 128 additions & 0 deletions invenio/modules/documents/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 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.

"""Document utils."""

from flask import abort

__all__ = ('identifier_to_path', 'identifier_to_path_and_permissions', )


def identifier_to_path(identifier):
"""Convert the identifier to path.
:param str identifier: A unique file identifier
:raises NotFound: if the file fullpath is empty
"""
fullpath = identifier_to_path_and_permissions(identifier, path_only=True)
return fullpath or abort(404)


def identifier_to_path_and_permissions(identifier, path_only=False):
"""Convert the identifier to path.
:param str identifier: A unique file identifier
:raises Forbidden: if the user doesn't have permissions
"""
if identifier.startswith('recid:'):
record_id, filename = _parse_legacy_syntax(identifier)

fullpath, permissions = _get_legacy_bibdoc(
record_id, filename=filename
)
else:
fullpath, permissions = _get_document(identifier)

if path_only:
return fullpath

try:
assert (permissions[0] == 0)
except AssertionError:
return abort(403)


def _get_document(uuid):
"""Get the document fullpath.
:param str uuid: The document's uuid
"""
from invenio.modules.documents.api import Document
from invenio.modules.documents.errors import (
DocumentNotFound, DeletedDocument
)

try:
document = Document.get_document(uuid)
except (DocumentNotFound, DeletedDocument):
path = _simulate_file_not_found()
else:
path = document.get('uri', ''), document.is_authorized()
finally:
return path


def _get_legacy_bibdoc(recid, filename=None):
"""Get the the fullpath of legacy bibdocfile.
:param int recid: The record id
:param str filename: A specific filename
:returns: bibdocfile full path
:rtype: str
"""
from invenio.ext.login import current_user
from invenio.legacy.bibdocfile.api import BibRecDocs
paths = [
(bibdoc.fullpath, bibdoc.is_restricted(current_user))
for bibdoc in BibRecDocs(recid).list_latest_files(list_hidden=False)
if not bibdoc.subformat and not filename or
bibdoc.name + bibdoc.superformat == filename
]
try:
path = paths[0]
except IndexError:
path = _simulate_file_not_found()
finally:
return path


def _parse_legacy_syntax(identifier):
"""Parse legacy syntax.
.. note::
It can handle requests such as `recid:{recid}` or
`recid:{recid}-{filename}`.
"""
if '-' in identifier:
record_id, filename = identifier.split('recid:')[1].split('-')
else:
record_id, filename = identifier.split('recid:')[1], None
return record_id, filename


def _simulate_file_not_found():
"""Simulate file not found situation.
..note ::
It simulates an file not found situation, this will always raise `404`
error.
"""
return '', (0, '')
1 change: 1 addition & 0 deletions requirements-devel.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
-e git+git://github.com/inveniosoftware/flask-menu.git#egg=flask-menu
-e git+git://github.com/inveniosoftware/flask-breadcrumbs.git#egg=flask-breadcrumbs
-e git+git://github.com/inveniosoftware/flask-registry.git#egg=flask-registry
-e git+git://github.com/inveniosoftware/flask-iiif.git#egg=flask-iiif
-e git+git://github.com/inveniosoftware/invenio-query-parser.git#egg=invenio-query-parser
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def run(self):
"Flask-Collect>=1.1.1",
"Flask-Email>=1.4.4",
"Flask-Gravatar>=0.4.2",
"Flask-IIIF>=0.2.0",
"Flask-Login>=0.2.7",
"Flask-Menu>=0.2",
"Flask-OAuthlib>=0.6.0,<0.7", # quick fix for issue #2158
Expand Down

0 comments on commit 213b6f1

Please sign in to comment.