class BucketLockedError(InvalidOperationError):
    """Exception raised when a bucket is locked."""

    code = 403
    description = "Bucket is locked for modifications." class FileInstanceAlreadySetError(InvalidOperationError):
    """Exception raised when file instance already set on object.""" class FileInstanceUnreadableError(InvalidOperationError):
    """Exception raised when trying to get an unreadable file."""

    code = 503
    description = "File storage is offline." class FileSizeError(StorageError):
    """Exception raised when a file larger than allowed."""

    code = 400 class InvalidKeyError(InvalidOperationError):
    """Invalid key."""

    code = 400
    description = "Filename is too long." class InvalidOperationError(FilesException):
    """Exception raised when an invalid operation is performed."""

    code = 403 class MultipartAlreadyCompleted(MultipartException):
    """Exception raised when multipart object is already completed."""

    code = 403
    description = "Multipart upload is already completed." class MultipartInvalidChunkSize(MultipartException):
    """Exception raised when multipart object is already completed."""

    code = 400
    description = "Invalid part size." class MultipartInvalidPartNumber(MultipartException):
    """Exception raised when multipart object is already completed."""

    code = 400
    description = "Invalid part number." class MultipartInvalidSize(MultipartException):
    """Exception raised when multipart object is already completed."""

    code = 400
    description = "Invalid file size." class MultipartMissingParts(MultipartException):
    """Exception raised when multipart object is already completed."""

    code = 400
    description = "Not all parts have been uploaded." class MultipartNotCompleted(MultipartException):
    """Exception raised when multipart object is not already completed."""

    code = 400
    description = "Multipart upload is already completed." def guess_mimetype(filename):
    """Map extra mimetype with the encoding provided.

    :returns: The extra mimetype.
    """
    m, encoding = mimetypes.guess_type(filename)
    if encoding:
        m = ENCODING_MIMETYPES.get(encoding, None)
    return m or 'application/octet-stream'
import re
import sys
import uuid
from datetime import datetime
from flask import current_app
from functools import wraps
from invenio_db import db
from os.path import basename
from sqlalchemy.dialects import mysql
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import validates
from sqlalchemy.orm.exc import MultipleResultsFound
from sqlalchemy_utils.types import UUIDType
from .errors import BucketLockedError, FileInstanceAlreadySetError, \
    FileInstanceUnreadableError, FileSizeError, InvalidKeyError, \
    InvalidOperationError, MultipartAlreadyCompleted, \
    MultipartInvalidChunkSize, MultipartInvalidPartNumber, \
    MultipartInvalidSize, MultipartMissingParts, MultipartNotCompleted
from .proxies import current_files_rest
from .utils import guess_mimetype
        db.Text().with_variant(mysql.VARCHAR(255), 'mysql'),
    )
    """Key identifying the object."""

    file_id = db.Column(
        UUIDType,
        db.ForeignKey(FileInstance.id, ondelete='RESTRICT'), nullable=False)
    """File instance for this multipart object."""

    chunk_size = db.Column(db.Integer, nullable=True)
    """Size of chunks for file."""

    size = db.Column(db.BigInteger, nullable=True)
    """Size of file."""

    completed = db.Column(db.Boolean(name='completed'),
                          nullable=False,
                          default=False)
    """Defines if object is the completed."""

    # Relationships definitions
    bucket = db.relationship(Bucket, backref='multipart_objects')
    """Relationship to buckets."""

    file = db.relationship(FileInstance, backref='multipart_objects')
    """Relationship to buckets."""

    def __repr__(self):
        """Return representation of the multipart object."""
        return "{0}:{2}:{1}".format(
            self.bucket_id, self.key, self.upload_id)

    @property
    def last_part_number(self):
        """Get last part number."""
        return int(self.size / self.chunk_size) \
            if self.size % self.chunk_size else \
            int(self.size / self.chunk_size) - 1

    @property
    def last_part_size(self):
        """Get size of last part."""
        return self.size % self.chunk_size

    @validates('key')
    def validate_key(self, key, key_):
        """Validate key."""
        return validate_key(key_)

    @staticmethod
    def is_valid_chunksize(chunk_size):
        """Check if size is valid."""
        min_csize = current_app.config['FILES_REST_MULTIPART_CHUNKSIZE_MIN']
        max_csize = current_app.config['FILES_REST_MULTIPART_CHUNKSIZE_MAX']
        return chunk_size >= min_csize and chunk_size <= max_csize

    @staticmethod
    def is_valid_size(size, chunk_size):
        """Validate max theoretical size."""
        min_csize = current_app.config['FILES_REST_MULTIPART_CHUNKSIZE_MIN']
        max_size = \
            chunk_size * current_app.config['FILES_REST_MULTIPART_MAX_PARTS']
        return size > min_csize and size <= max_size

    def expected_part_size(self, part_number):
        """Get expected part size for a particular part number."""
        last_part = self.multipart.last_part_number

        if part_number == last_part:
            return self.multipart.last_part_size
        elif part_number >= 0 and part_number < last_part:
            return self.multipart.chunk_size
        else:
            raise MultipartInvalidPartNumber()

    @ensure_uncompleted()
    def complete(self):
        """Mark a multipart object as complete."""
        if Part.count(self) != self.last_part_number + 1:
            raise MultipartMissingParts()

        with db.session.begin_nested():
            self.completed = True
            self.file.readable = True
            self.file.writable = False
        return self

    @ensure_completed()
    def merge_parts(self, version_id=None, **kwargs):
        """Merge parts into object version."""
        self.file.update_checksum(**kwargs)
        with db.session.begin_nested():
            obj = ObjectVersion.create(
                self.bucket,
                self.key,
                _file_id=self.file_id,
                version_id=version_id
            )
            self.delete()
        return obj

    def delete(self):
        """Delete a multipart object."""
        # Update bucket size.
        self.bucket.size -= self.size
        # Remove parts
        Part.query_by_multipart(self).delete()
        # Remove self
        self.query.filter_by(upload_id=self.upload_id).delete()

    @classmethod
    def create(cls, bucket, key, size, chunk_size):
        """Create a new object in a bucket."""
        bucket = as_bucket(bucket)

        if bucket.locked:
            raise BucketLockedError()

        # Validate chunk size.
        if not cls.is_valid_chunksize(chunk_size):

            raise MultipartInvalidChunkSize()