Source code for invenio_records_files.api

# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 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.

"""API for manipulating files associated to a record."""

from collections import OrderedDict
from functools import wraps

from invenio_db import db
from invenio_files_rest.errors import InvalidOperationError
from invenio_files_rest.models import ObjectVersion
from invenio_records.api import Record as _Record
from invenio_records.errors import MissingModelError

from .models import RecordsBuckets
from .utils import sorted_files_from_bucket


[docs]class FileObject(object): """Wrapper for files.""" def __init__(self, obj, data): """Bind to current bucket.""" self.obj = obj self.data = data
[docs] def get_version(self, version_id=None): """Return specific version ``ObjectVersion`` instance or HEAD. :param version_id: Version ID of the object. :returns: :class:`~invenio_files_rest.models.ObjectVersion` instance or HEAD of the stored object. """ return ObjectVersion.get(bucket=self.obj.bucket, key=self.obj.key, version_id=version_id)
[docs] def get(self, key, default=None): """Proxy to ``obj``. :param key: Metadata key which holds the value. :returns: Metadata value of the specified key or default. """ if hasattr(self.obj, key): return getattr(self.obj, key) return self.data.get(key, default)
def __getattr__(self, key): """Proxy to ``obj``.""" return getattr(self.obj, key) def __getitem__(self, key): """Proxy to ``obj`` and ``data``.""" if hasattr(self.obj, key): return getattr(self.obj, key) return self.data[key] def __setitem__(self, key, value): """Proxy to ``data``.""" if hasattr(self.obj, key): raise KeyError(key) self.data[key] = value
[docs] def dumps(self): """Create a dump of the metadata associated to the record.""" self.data.update({ 'bucket': str(self.obj.bucket_id), 'checksum': self.obj.file.checksum, 'key': self.obj.key, # IMPORTANT it must stay here! 'size': self.obj.file.size, 'version_id': str(self.obj.version_id), }) return self.data
def _writable(method): """Check that record is in defined status. :param method: Method to be decorated. :returns: Function decorated. """ @wraps(method) def wrapper(self, *args, **kwargs): """Send record for indexing. :returns: Execution result of the decorated method. :raises InvalidOperationError: It occurs when the bucket is locked or deleted. """ if self.bucket.locked or self.bucket.deleted: raise InvalidOperationError() return method(self, *args, **kwargs) return wrapper
[docs]class FilesIterator(object): """Iterator for files.""" def __init__(self, record, bucket=None, file_cls=None): """Initialize iterator.""" self._it = None self.record = record self.model = record.model self.file_cls = file_cls or FileObject self.bucket = bucket self.filesmap = OrderedDict([ (f['key'], f) for f in self.record.get('_files', []) ]) @property def keys(self): """Return file keys.""" return self.filesmap.keys() def __len__(self): """Get number of files.""" return ObjectVersion.get_by_bucket(self.bucket).count() def __iter__(self): """Get iterator.""" self._it = iter(sorted_files_from_bucket(self.bucket, self.keys)) return self
[docs] def next(self): """Python 2.7 compatibility.""" return self.__next__() # pragma: no cover
def __next__(self): """Get next file item.""" obj = next(self._it) return self.file_cls(obj, self.filesmap.get(obj.key, {})) def __contains__(self, key): """Test if file exists.""" return ObjectVersion.get_by_bucket( self.bucket).filter_by(key=str(key)).count() def __getitem__(self, key): """Get a specific file.""" obj = ObjectVersion.get(self.bucket, key) if obj: return self.file_cls(obj, self.filesmap.get(obj.key, {})) raise KeyError(key)
[docs] def flush(self): """Flush changes to record.""" files = self.dumps() # Do not create `_files` when there has not been `_files` field before # and the record still has no files attached. if files or '_files' in self.record: self.record['_files'] = files
@_writable def __setitem__(self, key, stream): """Add file inside a deposit.""" with db.session.begin_nested(): # save the file obj = ObjectVersion.create( bucket=self.bucket, key=key, stream=stream) self.filesmap[key] = self.file_cls(obj, {}).dumps() self.flush() @_writable def __delitem__(self, key): """Delete a file from the deposit.""" obj = ObjectVersion.delete(bucket=self.bucket, key=key) if obj is None: raise KeyError(key) if key in self.filesmap: del self.filesmap[key] self.flush()
[docs] def sort_by(self, *ids): """Update files order. :param ids: List of ids specifying the final status of the list. """ # Support sorting by file_ids or keys. files = {str(f_.file_id): f_.key for f_ in self} # self.record['_files'] = [{'key': files.get(id_, id_)} for id_ in ids] self.filesmap = OrderedDict([ (files.get(id_, id_), self[files.get(id_, id_)].dumps()) for id_ in ids ]) self.flush()
@_writable
[docs] def rename(self, old_key, new_key): """Rename a file. :param old_key: Old key that holds the object. :param new_key: New key that will hold the object. :returns: The object that has been renamed. """ assert new_key not in self assert old_key != new_key file_ = self[old_key] old_data = self.filesmap[old_key] # Create a new version with the new name obj = ObjectVersion.create( bucket=self.bucket, key=new_key, _file_id=file_.obj.file_id ) # Delete old key self.filesmap[new_key] = self.file_cls(obj, old_data).dumps() del self[old_key] return obj
[docs] def dumps(self, bucket=None): """Serialize files from a bucket. :param bucket: Instance of files :class:`invenio_files_rest.models.Bucket`. (Default: ``self.bucket``) :returns: List of serialized files. """ return [ self.file_cls(o, self.filesmap.get(o.key, {})).dumps() for o in sorted_files_from_bucket(bucket or self.bucket, self.keys) ]
[docs]class FilesMixin(object): """Implement files attribute for Record models. .. note:: Implement ``_create_bucket()`` in subclass to allow files property to automatically create a bucket in case no bucket is present. """ file_cls = FileObject """File class used to generate the instance of files. Default to :class:`~invenio_records_files.api.FileObject` """ files_iter_cls = FilesIterator """Files iterator class used to generate the files iterator. Default to :class:`~invenio_records_files.api.FilesIterator` """ def _create_bucket(self): """Return an instance of ``Bucket`` class. .. note:: Reimplement in children class for custom behavior. :returns: Instance of :class:`invenio_files_rest.models.Bucket`. """ return None @property def files(self): """Get files iterator. :returns: Files iterator. """ if self.model is None: raise MissingModelError() records_buckets = RecordsBuckets.query.filter_by( record_id=self.id).first() if not records_buckets: bucket = self._create_bucket() if not bucket: return None RecordsBuckets.create(record=self.model, bucket=bucket) else: bucket = records_buckets.bucket return self.files_iter_cls(self, bucket=bucket, file_cls=self.file_cls) @files.setter def files(self, data): """Set files from data.""" current_files = self.files if current_files: raise RuntimeError('Can not update existing files.') for key in data: current_files[key] = data[key]
[docs]class Record(_Record, FilesMixin): """Define API for files manipulation using ``FilesMixin``."""
[docs] def delete(self, force=False): """Delete a record and also remove the RecordsBuckets if necessary. :param force: True to remove also the :class:`~invenio_records_files.models.RecordsBuckets` object. :returns: Deleted record. """ if force: RecordsBuckets.query.filter_by( record=self.model, bucket=self.files.bucket ).delete() return super(Record, self).delete(force)