Beispiel #1
0
Datei: tasks.py Projekt: quru/qis
def delete_folder_data(**kwargs):
    """
    A task to delete (with purge True) or mark as deleted (with purge False)
    all data (folder, sub-folders, and images within) contained within a
    folder.

    This method should only be used to tidy up the database when the disk
    folder has already been deleted. If you want to delete a folder
    (on disk and in the database) normally, use the delete_folder() method.

    Raises an AssertionError if the folder still exists on disk
    Raises a DBError for database errors.
    """
    from flask_app import app
    from filesystem_manager import path_exists

    (folder_id, purge, history_user, history_info) = _extract_parameters(
        ["folder_id", "purge", "history_user", "history_info"], **kwargs
    )

    # Get the folder to delete
    db_folder = app.data_engine.get_folder(folder_id)
    if not db_folder:
        app.log.warn("Folder ID %d has already been deleted" % folder_id)
        return
    # Don't continue if the folder still exists on disk
    assert not path_exists(db_folder.path, require_directory=True), "Folder %s still exists on disk!" % db_folder.path
    # Otherwise carry on
    app.log.info("Deleting data for missing disk folder " + db_folder.path)
    app.data_engine.delete_folder(db_folder, purge=purge, history_user=history_user, history_info=history_info)
Beispiel #2
0
def upload_form():
    # Get upload directory options, and convert path templates to actual paths
    upload_dirs = [
        get_upload_directory(i) for i in range(len(app.config['IMAGE_UPLOAD_DIRS']))
    ]
    # Add in whether the user is allowed to upload and view
    upload_dirs = [(
        udir[0],
        udir[1],
        permissions_engine.is_folder_permitted(
            udir[1],
            FolderPermission.ACCESS_UPLOAD,
            get_session_user(),
            folder_must_exist=False
        ),
        permissions_engine.is_folder_permitted(
            udir[1],
            FolderPermission.ACCESS_VIEW,
            get_session_user(),
            folder_must_exist=False
        )
    ) for udir in upload_dirs]

    # Determine which radio button to pre-select
    dir_idx = -1
    to_path = request.args.get('path', None)
    if not to_path or to_path == os.path.sep:
        # Default to the first entry that allows upload
        for (idx, udir) in enumerate(upload_dirs):
            if udir[2]:
                dir_idx = idx
                break
    else:
        # Try matching the defined paths
        to_path = strip_seps(to_path)
        for (idx, udir) in enumerate(upload_dirs):
            if strip_seps(udir[1]) == to_path:
                dir_idx = idx
                break

    # If it's a manual path, use it only if it exists
    manual_path = ''
    if dir_idx == -1 and to_path and path_exists(to_path, require_directory=True):
        manual_path = to_path

    return render_template(
        'upload.html',
        upload_dirs=upload_dirs,
        sel_radio_num=dir_idx,
        manual_path=manual_path
    )
Beispiel #3
0
Datei: tasks.py Projekt: quru/qis
def burst_pdf(**kwargs):
    """
    A task that creates a sub-folder next to a PDF file and extracts all
    pages from the PDF as PNG files into the sub-folder.
    """
    from flask_app import app
    from filesystem_manager import get_abs_path, get_burst_path, get_file_data
    from filesystem_manager import delete_dir, make_dirs, path_exists
    from filesystem_sync import delete_folder
    from imagemagick import imagemagick_burst_pdf
    from models import Folder
    from util import get_file_extension

    (src,) = _extract_parameters(["src"], **kwargs)
    burst_folder_rel = get_burst_path(src)

    # Ensure src is a PDF
    if get_file_extension(src) not in app.config["PDF_FILE_TYPES"]:
        app.log.warn("Cannot burst non-PDF file: " + src)
        return

    # See if the burst folder already exists (in the database and on disk)
    db_folder = app.data_engine.get_folder(folder_path=burst_folder_rel)
    if db_folder is not None and db_folder.status == Folder.STATUS_ACTIVE:
        # Wipe the folder, old images, data, and uncache the old images
        delete_folder(db_folder, None, app.data_engine, None, app.log)
        deleted_ids = app.data_engine.list_image_ids(db_folder)
        for image_id in deleted_ids:
            app.image_engine._uncache_image_id(image_id)

    # See if the burst folder already exists (just on disk)
    if path_exists(burst_folder_rel, require_directory=True):
        # Wipe the folder and old images
        delete_dir(burst_folder_rel, recursive=True)

    # Create the burst folder and burst
    pdf_data = get_file_data(src)
    if pdf_data is not None:
        make_dirs(burst_folder_rel)
        burst_folder_abs = get_abs_path(burst_folder_rel)
        if not imagemagick_burst_pdf(pdf_data, burst_folder_abs, app.config["PDF_BURST_DPI"]):
            app.log.warn("Failed to burst PDF: " + src)
    else:
        app.log.warn("Cannot burst PDF, file not found: " + src)
Beispiel #4
0
def _secure_folder_path(folderpath, skip_existing, keep_unicode):
    """
    Splits a folder path, runs each component through the secure_filename
    function, and returns the reconstituted folder path. If skip_existing
    is True, components of the path that already exist will not be modified.
    Raises a ValueError if any of the path components becomes empty.
    """
    _, _, f_list = filepath_components(add_sep(folderpath))
    built_path = ''
    for component in f_list:
        if component:
            next_path = os.path.join(built_path, component)
            if not skip_existing or not path_exists(next_path, require_directory=True):
                try:
                    component = secure_filename(component, keep_unicode)
                except ValueError as e:
                    raise ValueError(unicode(e) + u': ' + component)
            built_path = os.path.join(built_path, component)
    return built_path
Beispiel #5
0
Datei: views.py Projekt: quru/qis
def image():
    logger.debug(request.method + ' ' + request.url)
    try:
        logged_in = session_logged_in()
        allow_uncache = app.config['BENCHMARKING'] or app.config['DEBUG']
        args = request.args

        # Get URL parameters for the image
        src         = args.get('src', '')
        page        = args.get('page', None)
        iformat     = args.get('format', None)
        template    = args.get('tmp', None)
        width       = args.get('width', None)
        height      = args.get('height', None)
        halign      = args.get('halign', None)
        valign      = args.get('valign', None)
        autosizefit = args.get('autosizefit', None)
        rotation    = args.get('angle', None)
        flip        = args.get('flip', None)
        top         = args.get('top', None)
        left        = args.get('left', None)
        bottom      = args.get('bottom', None)
        right       = args.get('right', None)
        autocropfit = args.get('autocropfit', None)
        fill        = args.get('fill', None)
        quality     = args.get('quality', None)
        sharpen     = args.get('sharpen', None)
        ov_src      = args.get('overlay', None)
        ov_size     = args.get('ovsize', None)
        ov_opacity  = args.get('ovopacity', None)
        ov_pos      = args.get('ovpos', None)
        icc_profile = args.get('icc', None)
        icc_intent  = args.get('intent', None)
        icc_bpc     = args.get('bpc', None)
        colorspace  = args.get('colorspace', None)
        strip       = args.get('strip', None)
        dpi         = args.get('dpi', None)
        tile        = args.get('tile', None)
        # Get URL parameters for handling options
        attach      = args.get('attach', None)
        xref        = args.get('xref', None)
        stats       = args.get('stats', None)
        # Get protected admin/internal parameters
        cache       = args.get('cache', '1') if logged_in or allow_uncache else '1'
        recache     = args.get('recache', None) if allow_uncache else None

        # eRez compatibility mode
        src = erez_params_compat(src)

        # Tweak strings as necessary and convert non-string parameters
        # to the correct data types
        try:
            # Image options
            if page is not None:
                page = parse_int(page)
            if iformat is not None:
                iformat = iformat.lower()
            if template is not None:
                template = template.lower()
            if width is not None:
                width = parse_int(width)
            if height is not None:
                height = parse_int(height)
            if halign is not None:
                halign = halign.lower()
            if valign is not None:
                valign = valign.lower()
            if autosizefit is not None:
                autosizefit = parse_boolean(autosizefit)
            if rotation is not None:
                rotation = parse_float(rotation)
            if flip is not None:
                flip = flip.lower()
            if top is not None:
                top = parse_float(top)
            if left is not None:
                left = parse_float(left)
            if bottom is not None:
                bottom = parse_float(bottom)
            if right is not None:
                right = parse_float(right)
            if autocropfit is not None:
                autocropfit = parse_boolean(autocropfit)
            if fill is not None:
                fill = parse_colour(fill)
            if quality is not None:
                quality = parse_int(quality)
            if sharpen is not None:
                sharpen = parse_int(sharpen)
            if ov_size is not None:
                ov_size = parse_float(ov_size)
            if ov_pos is not None:
                ov_pos = ov_pos.lower()
            if ov_opacity is not None:
                ov_opacity = parse_float(ov_opacity)
            if icc_profile is not None:
                icc_profile = icc_profile.lower()
            if icc_intent is not None:
                icc_intent = icc_intent.lower()
            if icc_bpc is not None:
                icc_bpc = parse_boolean(icc_bpc)
            if colorspace is not None:
                colorspace = colorspace.lower()
            if strip is not None:
                strip = parse_boolean(strip)
            if dpi is not None:
                dpi = parse_int(dpi)
            if tile is not None:
                tile = parse_tile_spec(tile)
            # Handling options
            if attach is not None:
                attach = parse_boolean(attach)
            if xref is not None:
                validate_string(xref, 0, 1024)
            if stats is not None:
                stats = parse_boolean(stats)
            # Admin/internal options
            if cache is not None:
                cache = parse_boolean(cache)
            if recache is not None:
                recache = parse_boolean(recache)
        except (ValueError, TypeError) as e:
            raise httpexc.BadRequest(unicode(e))

        # Package and validate the parameters
        try:
            # #2694 Enforce public image limits - perform easy parameter checks
            if not logged_in:
                width, height, autosizefit = _public_image_limits_pre_image_checks(
                    width, height, autosizefit, tile, template
                )
            # Store and normalise all the parameters
            image_attrs = ImageAttrs(src, -1, page, iformat, template,
                                     width, height, halign, valign,
                                     rotation, flip,
                                     top, left, bottom, right, autocropfit,
                                     autosizefit, fill, quality, sharpen,
                                     ov_src, ov_size, ov_pos, ov_opacity,
                                     icc_profile, icc_intent, icc_bpc,
                                     colorspace, strip, dpi, tile)
            image_engine.finalise_image_attrs(image_attrs)
        except ValueError as e:
            raise httpexc.BadRequest(unicode(e))

        # Get/create the database ID (from cache, validating path on create)
        image_id = data_engine.get_or_create_image_id(
            image_attrs.filename(),
            return_deleted=False,
            on_create=on_image_db_create_anon_history
        )
        if (image_id == 0):
            raise DoesNotExistError()  # Deleted
        elif (image_id < 0):
            raise DBError('Failed to add image to database')
        image_attrs.set_database_id(image_id)

        # Require view permission or file admin
        permissions_engine.ensure_folder_permitted(
            image_attrs.folder_path(),
            FolderPermission.ACCESS_VIEW,
            get_session_user()
        )
        # Ditto for overlays
        if ov_src:
            permissions_engine.ensure_folder_permitted(
                filepath_parent(ov_src),
                FolderPermission.ACCESS_VIEW,
                get_session_user()
            )

        # v1.17 If this is a conditional request with an ETag, see if we can just return a 304
        if 'If-None-Match' in request.headers and not recache:
            etag_valid, modified_time = _etag_is_valid(
                image_attrs,
                request.headers['If-None-Match'],
                False
            )
            if etag_valid:
                # Success HTTP 304
                return make_304_response(image_attrs, False, modified_time)

        # Get the requested image data
        image_wrapper = image_engine.get_image(
            image_attrs,
            'refresh' if recache else cache
        )
        if (image_wrapper is None):
            raise DoesNotExistError()

        # #2694 Enforce public image limits - check the dimensions
        #       of images that passed the initial parameter checks
        if not logged_in:
            try:
                _public_image_limits_post_image_checks(
                    image_attrs.width(),
                    image_attrs.height(),
                    image_attrs.template(),
                    image_wrapper.data(),
                    image_wrapper.attrs().format()
                )
            except ValueError as e:
                raise httpexc.BadRequest(unicode(e))  # As for the pre-check

        # Success HTTP 200
        return make_image_response(image_wrapper, False, stats, attach, xref)
    except httpexc.HTTPException:
        # Pass through HTTP 4xx and 5xx
        raise
    except ServerTooBusyError:
        logger.warn(u'503 Too busy for ' + request.url)
        raise httpexc.ServiceUnavailable()
    except ImageError as e:
        logger.warn(u'415 Invalid image file \'' + src + '\' : ' + unicode(e))
        raise httpexc.UnsupportedMediaType(unicode(e))
    except SecurityError as e:
        if app.config['DEBUG']:
            raise
        log_security_error(e, request)
        raise httpexc.Forbidden()
    except DoesNotExistError as e:
        # First time around the ID will be set. Next time around it
        # won't but we should check whether the disk file now exists.
        if image_attrs.database_id() > 0 or path_exists(image_attrs.filename(), require_file=True):
            image_engine.reset_image(image_attrs)
        logger.warn(u'404 Not found: ' + unicode(e))
        raise httpexc.NotFound(unicode(e))
    except Exception as e:
        if app.config['DEBUG']:
            raise
        logger.error(u'500 Error for ' + request.url + '\n' + unicode(e))
        raise httpexc.InternalServerError(unicode(e))
Beispiel #6
0
Datei: views.py Projekt: quru/qis
def original():
    logger.debug('GET ' + request.url)
    try:
        # Get URL parameters for the image
        src = request.args.get('src', '')
        # Get URL parameters for handling options
        attach  = request.args.get('attach', None)
        xref    = request.args.get('xref', None)
        stats   = request.args.get('stats', None)

        # Validate the parameters
        try:
            if attach is not None:
                attach = parse_boolean(attach)
            if xref is not None:
                validate_string(xref, 0, 1024)
            if stats is not None:
                stats = parse_boolean(stats)

            image_attrs = ImageAttrs(src)
            image_attrs.validate()
        except ValueError as e:
            raise httpexc.BadRequest(unicode(e))

        # Get/create the database ID (from cache, validating path on create)
        image_id = data_engine.get_or_create_image_id(
            image_attrs.filename(),
            return_deleted=False,
            on_create=on_image_db_create_anon_history
        )
        if (image_id == 0):
            raise DoesNotExistError()  # Deleted
        elif (image_id < 0):
            raise DBError('Failed to add image to database')
        image_attrs.set_database_id(image_id)

        # Require download permission or file admin
        permissions_engine.ensure_folder_permitted(
            image_attrs.folder_path(),
            FolderPermission.ACCESS_DOWNLOAD,
            get_session_user()
        )

        # v1.17 If this is a conditional request with an ETag, see if we can just return a 304
        if 'If-None-Match' in request.headers:
            etag_valid, modified_time = _etag_is_valid(
                image_attrs,
                request.headers['If-None-Match'],
                True
            )
            if etag_valid:
                # Success HTTP 304
                return make_304_response(image_attrs, True, modified_time)

        # Read the image file
        image_wrapper = image_engine.get_image_original(
            image_attrs
        )
        if (image_wrapper is None):
            raise DoesNotExistError()

        # Success HTTP 200
        return make_image_response(image_wrapper, True, stats, attach, xref)
    except httpexc.HTTPException:
        # Pass through HTTP 4xx and 5xx
        raise
    except ServerTooBusyError:
        logger.warn(u'503 Too busy for ' + request.url)
        raise httpexc.ServiceUnavailable()
    except ImageError as e:
        logger.warn(u'415 Invalid image file \'' + src + '\' : ' + unicode(e))
        raise httpexc.UnsupportedMediaType(unicode(e))
    except SecurityError as e:
        if app.config['DEBUG']:
            raise
        log_security_error(e, request)
        raise httpexc.Forbidden()
    except DoesNotExistError as e:
        # First time around the ID will be set. Next time around it
        # won't but we should check whether the disk file now exists.
        if image_attrs.database_id() > 0 or path_exists(image_attrs.filename(), require_file=True):
            image_engine.reset_image(image_attrs)
        logger.warn(u'404 Not found: ' + src)
        raise httpexc.NotFound(src)
    except Exception as e:
        if app.config['DEBUG']:
            raise
        logger.error(u'500 Error for ' + request.url + '\n' + unicode(e))
        raise httpexc.InternalServerError(unicode(e))
Beispiel #7
0
def auto_sync_file(rel_path, data_manager, task_manager,
                   anon_history=True, burst_pdf='auto', _db_session=None):
    """
    Returns the database record for an image file, creating a new record if
    required, otherwise syncing the status flag with the existence of the file.
    Returns None if the file does not exist and there is also no database record
    for the path. Otherwise the status flag of the returned image record indicates
    whether the disk file still exists.

    This method creates anonymous image history entries when anon_history is
    True. If the current user should be recorded against an action, the caller
    should set anon_history to False and manually add a history record.

    The bursting of PDF files is also initiated here. If the file exists and is a
    PDF, by default it will be burst if no burst folder already exists. Setting
    burst_pdf to False disables this, or setting burst_pdf to True will force it
    to be burst again.

    Raises a SecurityError if the image path is outside of IMAGES_BASE_DIR.
    Raises a DBError if the database record cannot be created.
    """
    db_own = (_db_session is None)
    db_session = _db_session or data_manager.db_get_session()
    db_error = False
    try:
        if path_exists(rel_path, require_file=True):
            return auto_sync_existing_file(
                rel_path,
                data_manager,
                task_manager,
                anon_history,
                burst_pdf,
                _db_session=db_session
            )
        else:
            # No file on disk; see how that compares with the database
            db_image = data_manager.get_image(src=rel_path, _db_session=db_session)
            if not db_image:
                # No file, no database record
                return None
            elif db_image.status == Image.STATUS_DELETED:
                # Database record is already deleted
                return db_image
            else:
                # We need to delete the database record
                data_manager.delete_image(
                    db_image,
                    purge=False,
                    _db_session=db_session,
                    _commit=False
                )
                # Add history
                if anon_history:
                    data_manager.add_image_history(
                        db_image, None, ImageHistory.ACTION_DELETED,
                        'File not found: ' + rel_path,
                        _db_session=db_session,
                        _commit=False
                    )
                # Check whether the file's folder might need to be deleted too
                if db_image.folder.status == Folder.STATUS_ACTIVE:
                    auto_sync_folder(
                        db_image.folder.path,
                        data_manager,
                        task_manager,
                        _db_session=db_session
                    )
                return db_image
    except:
        db_error = True
        raise
    finally:
        if db_own:
            try:
                if db_error:
                    db_session.rollback()
                else:
                    db_session.commit()
            finally:
                db_session.close()
Beispiel #8
0
def move_folder(db_folder, target_path, user_account, data_manager, permissions_manager, logger):
    """
    Moves a disk folder to the given new path (which must not already exist),
    and updates the associated database records. The folder is effectively
    renamed if the parent folder path remains the same.

    This method may take a long time, as the folder's sub-folders and images
    must also be moved, both on disk and in the database. The audit trail is
    also updated for every affected image, image IDs cached under the old path
    are cleared, and folder tree permissions are re-calculated.

    The user account must have Delete Folder permission for the original
    parent folder and Create Folder permission for the target parent folder,
    or alternatively have the file admin system permission.

    This method creates and commits its own separate database connection
    in an attempt to keep the operation is as atomic as possible. Note however
    that if there is an error moving the folder tree (in the database or on
    disk), operations already performed are not rolled back, and the database
    may become out of sync with the file system.

    Returns the updated folder object, including all affected sub-folders.

    Raises a DoesNotExistError if the source folder does not exist.
    Raises an AlreadyExistsError if the target path already exists.
    Raises an IOError or OSError on error moving the disk files or folders.
    Raises a ValueError if the source folder or target path is invalid.
    Raises a DBError for database errors.
    Raises a SecurityError if the current user does not have sufficient
    permission to perform the move or if the target path is outside of
    IMAGES_BASE_DIR.
    """
    db_session = data_manager.db_get_session()
    success = False
    try:
        _validate_path_chars(target_path)
        target_path = filepath_normalize(target_path)
        target_path = _secure_folder_path(
            target_path,
            True,
            app.config['ALLOW_UNICODE_FILENAMES']
        )
        norm_src = strip_seps(db_folder.path)
        norm_tgt = strip_seps(target_path)

        # Cannot move the root folder
        if norm_src == '':
            raise ValueError('Cannot move the root folder')
        # Don't allow blank path (move to become root) either
        if norm_tgt == '':
            raise ValueError('Target folder path cannot be empty')
        # Cannot move a folder into itself
        if norm_tgt.startswith(add_sep(norm_src)):
            raise ValueError('Cannot move a folder into itself')

        # Do nothing if target path is the same as the source
        if norm_src == norm_tgt:
            success = True
            return db_folder

        # Connect db_folder to our database session
        db_folder = data_manager.get_folder(db_folder.id, _db_session=db_session)
        if not db_folder:
            raise DoesNotExistError('Folder ID %d does not exist' % db_folder.id)

        # Source folder must exist
        ensure_path_exists(db_folder.path, require_directory=True)
        # Target folder must not yet exist (we cannot merge)
        if path_exists(target_path):
            raise AlreadyExistsError('Path already exists: ' + target_path)

        renaming = (
            strip_seps(filepath_parent(db_folder.path)) ==
            strip_seps(filepath_parent(target_path))
        )

        # Get parent folders for permissions checking
        # Target parent may not exist yet so use the closest node in the tree
        db_source_parent = db_folder.parent
        db_target_parent = _get_nearest_parent_folder(
            target_path, data_manager, db_session
        )
        # Require Create Folder permission for destination folder
        if user_account:
            permissions_manager.ensure_folder_permitted(
                db_target_parent,
                FolderPermission.ACCESS_CREATE_FOLDER,
                user_account
            )
        # Require Delete Folder permission for source parent folder
        if user_account and not renaming:
            permissions_manager.ensure_folder_permitted(
                db_source_parent,
                FolderPermission.ACCESS_DELETE_FOLDER,
                user_account
            )

        logger.info(
            'Disk folder %s is being moved to %s by %s' %
            (db_folder.path, target_path,
             user_account.username if user_account else 'System')
        )

        # We know there's no physical target folder, but if there is an
        # old (deleted) db record for the target path, purge it first.
        db_old_target_folder = data_manager.get_folder(
            folder_path=target_path, _db_session=db_session
        )
        if db_old_target_folder:
            # This recurses to purge files and sub-folders too
            data_manager.delete_folder(
                db_old_target_folder,
                purge=True,
                _db_session=db_session,
                _commit=False
            )

        # Move the disk files first, as this is the most likely thing to fail.
        # Note that this might involve moving files and directories we haven't
        # got database entries for (but that doesn't matter).
        filesystem_manager.move(db_folder.path, target_path)

        # Prep image history
        if renaming:
            history_info = 'Folder renamed from ' + filepath_filename(db_folder.path) + \
                           ' to ' + filepath_filename(target_path)
        else:
            history_info = 'Folder moved from ' + db_folder.path + ' to ' + target_path

        # Update the database
        data_manager.set_folder_path(
            db_folder,
            target_path,
            user_account,
            history_info,
            _db_session=db_session,
            _commit=False
        )

        # OK!
        logger.info(
            'Disk folder %s successfully moved to %s by %s' %
            (db_folder.path, target_path,
             user_account.username if user_account else 'System')
        )
        success = True
        return db_folder

    finally:
        # Commit or rollback database
        try:
            if success:
                db_session.commit()
            else:
                db_session.rollback()
        finally:
            db_session.close()

        # Clear folder permissions cache as folder tree has changed
        if success:
            permissions_manager.reset()
Beispiel #9
0
def create_folder(rel_path, user_account, data_manager, permissions_manager, logger):
    """
    Creates a folder on disk and the associated database record.
    The folder path cannot be blank and should not already exist.

    The user account must have Create Folder permission for the parent folder,
    or alternatively have the file admin system permission.

    This method creates and commits its own separate database connection
    so that the operation is atomic.

    Returns the new folder object.

    Raises an AlreadyExistsError if the folder path already exists.
    Raises an OSError if the new folder cannot be created.
    Raises a ValueError if the folder path is invalid.
    Raises a DBError for database errors.
    Raises a SecurityError if the current user does not have sufficient
    permission to create the folder, or if the folder path is outside of
    IMAGES_BASE_DIR.
    """
    db_session = data_manager.db_get_session()
    success = False
    try:
        _validate_path_chars(rel_path)
        rel_path = filepath_normalize(rel_path)
        rel_path = _secure_folder_path(
            rel_path,
            True,
            app.config['ALLOW_UNICODE_FILENAMES']
        )

        # Don't allow blank path
        if strip_seps(rel_path) == '':
            raise ValueError('Folder path to create cannot be empty')
        # Check for existing (physical) path
        if path_exists(rel_path):
            raise AlreadyExistsError('Path already exists: ' + rel_path)

        # Check permissions for the (nearest existing) db parent folder
        if user_account:
            db_parent_folder = _get_nearest_parent_folder(
                rel_path, data_manager, db_session
            )
            permissions_manager.ensure_folder_permitted(
                db_parent_folder,
                FolderPermission.ACCESS_CREATE_FOLDER,
                user_account
            )

        # Create the physical folder
        filesystem_manager.make_dirs(rel_path)

        # Update the database
        db_folder = auto_sync_existing_folder(
            rel_path,
            data_manager,
            _db_session=db_session
        )

        # OK!
        logger.info(
            'Disk folder %s created by %s' %
            (rel_path, user_account.username if user_account else 'System')
        )
        success = True
        return db_folder

    finally:
        # Commit or rollback database
        try:
            if success:
                db_session.commit()
            else:
                db_session.rollback()
        finally:
            db_session.close()
Beispiel #10
0
def move_file(db_image, target_path, user_account, data_manager, permissions_manager):
    """
    Moves an image file to the given new path and filename (the folder component
    of which must already exist), adds image history and updates the associated
    database records. The image file is effectively renamed if the folder part
    of the path remains the same.

    The user account must have Delete File permission for the source folder
    and Upload File permission for the target folder, or alternatively have
    the file admin system permission.

    This method creates and commits its own separate database connection
    so that the operation is atomic.

    Returns the updated image object.

    Raises a DoesNotExistError if the source image file does not exist
    or the target folder does not exist.
    Raises an AlreadyExistsError if the target file already exists.
    Raises an IOError or OSError if the target file cannot be created.
    Raises a ValueError if the target filename is invalid.
    Raises a DBError for database errors.
    Raises a SecurityError if the current user does not have sufficient
    permission to perform the move or if the target path is outside of
    IMAGES_BASE_DIR.
    """
    db_session = data_manager.db_get_session()
    file_moved = False
    success = False
    try:
        _validate_path_chars(target_path)
        target_path = filepath_normalize(target_path)

        # Connect db_image to our database session
        db_image = data_manager.get_image(db_image.id, _db_session=db_session)
        if not db_image:
            raise DoesNotExistError('Image ID %d does not exist' % db_image.id)

        # Save the old path for rolling back
        source_path = db_image.src
        source_folder = filepath_parent(source_path)
        source_filename = filepath_filename(source_path)

        # Get and secure the target filename
        target_folder = filepath_parent(target_path)
        target_filename = filepath_filename(target_path)
        target_filename = secure_filename(
            target_filename,
            app.config['ALLOW_UNICODE_FILENAMES']
        )
        target_path = os.path.join(target_folder, target_filename)

        # Insist on minimum a.xyz file name (else raise ValueError)
        validate_filename(target_filename)
        # Target folder must exist
        ensure_path_exists(target_folder, require_directory=True)

        # Do nothing if target path is the same as the source
        if strip_sep(db_image.src, leading=True) == strip_sep(target_path, leading=True):
            success = True
            return db_image

        # Get source and target folder data
        db_source_folder = db_image.folder
        db_target_folder = auto_sync_existing_folder(
            target_folder, data_manager, _db_session=db_session
        )
        if db_target_folder is None:
            raise DoesNotExistError(target_folder)  # Should never happen

        # Check source file exists
        ensure_path_exists(db_image.src, require_file=True)
        # Check target file does not exist (we cannot merge)
        if path_exists(target_path, require_file=True):
            raise AlreadyExistsError('Target file already exists: ' + target_path)

        renaming = (db_source_folder == db_target_folder)

        # Check permissions for source and destination folders
        permissions_manager.ensure_folder_permitted(
            db_target_folder,
            FolderPermission.ACCESS_UPLOAD,
            user_account
        )
        if not renaming:
            permissions_manager.ensure_folder_permitted(
                db_source_folder,
                FolderPermission.ACCESS_DELETE,
                user_account
            )

        # We know there's no physical target file, but if there is an
        # old (deleted) db record for the target path, purge it first
        db_old_target_image = data_manager.get_image(
            src=target_path, _db_session=db_session
        )
        if db_old_target_image:
            data_manager.delete_image(
                db_old_target_image,
                purge=True,
                _db_session=db_session,
                _commit=False
            )

        # Move the physical file
        filesystem_manager.move(source_path, target_path)
        file_moved = True

        # Update the database
        db_image.status = Image.STATUS_ACTIVE
        db_image.folder = db_target_folder
        data_manager.set_image_src(db_image, target_path)

        # Add history
        if renaming:
            history_info = 'Renamed from ' + source_filename + ' to ' + target_filename
        else:
            history_info = 'Moved from ' + source_folder + ' to ' + target_folder
        data_manager.add_image_history(
            db_image,
            user_account,
            ImageHistory.ACTION_MOVED,
            history_info,
            _db_session=db_session,
            _commit=False
        )

        # OK!
        success = True
        return db_image

    finally:
        # Rollback file move?
        if not success and file_moved:
            try:
                filesystem_manager.move(target_path, source_path)
            except:
                pass
        # Commit or rollback database
        try:
            if success:
                db_session.commit()
            else:
                db_session.rollback()
        finally:
            db_session.close()
Beispiel #11
0
def auto_sync_folder(rel_path, data_manager, task_manager,
                     anon_history=True, _db_session=None):
    """
    Returns the database record for a folder, creating a new record if required,
    otherwise syncing the status flag with the existence of the folder.
    Returns None if the folder does not exist and there is also no database record
    for the path. Otherwise the status flag of the returned folder record indicates
    whether the disk folder still exists.

    If the disk folder no longer exists, the database records for all sub-folders
    and images within are also marked as deleted. This is performed asynchronously
    as a background task. When marking the images as deleted, this method creates
    anonymous image history entries when anon_history is True. If the current
    user should be recorded against an action, the caller should set anon_history
    to False and manually add history records.

    Raises a SecurityError if the folder path is outside of IMAGES_BASE_DIR.
    Raises a DBError if the database record cannot be created.
    """
    db_own = (_db_session is None)
    db_session = _db_session or data_manager.db_get_session()
    db_error = False
    try:
        if path_exists(rel_path, require_directory=True):
            return auto_sync_existing_folder(
                rel_path,
                data_manager,
                _db_session=db_session
            )
        else:
            # No folder on disk; see how that compares with the database
            db_folder = data_manager.get_folder(folder_path=rel_path, _db_session=db_session)
            if not db_folder:
                # No folder, no database record
                return None
            elif db_folder.status == Folder.STATUS_DELETED:
                # Database record is already deleted
                return db_folder
            else:
                # We need to delete the database record and folder content.
                # This is done as an async task, as it can take a long time.
                task_manager.add_task(
                    None,
                    'Delete data for folder %d' % db_folder.id,
                    'delete_folder_data',
                    {
                        'folder_id': db_folder.id,
                        'purge': False,
                        'history_user': None,
                        'history_info': 'Containing folder deleted' if anon_history else None
                    },
                    Task.PRIORITY_NORMAL,
                    'info', 'error'
                )
                # But for returning now, set the status flag to deleted.
                # This will also prevent a duplicate delete task being created
                # the next time this function is called for the same folder.
                db_folder.status = Folder.STATUS_DELETED
                return db_folder
    except:
        db_error = True
        raise
    finally:
        if db_own:
            try:
                if db_error:
                    db_session.rollback()
                else:
                    db_session.commit()
            finally:
                db_session.close()
Beispiel #12
0
def auto_sync_existing_file(rel_path, data_manager, task_manager,
                            anon_history=True, burst_pdf='auto', _db_session=None):
    """
    Returns the database record for an image file that is known to exist,
    creating a new record or un-deleting an old record if required,
    and always returning a value.

    This method creates anonymous image history entries when anon_history is
    True. If the current user should be recorded against an action, the caller
    should set anon_history to False and manually add a history record.

    The bursting of PDF files is also initiated here. By default, a PDF file
    will be burst if no burst folder already exists. Setting burst_pdf to False
    disables this, or setting burst_pdf to True will force it to be burst again.

    Raises a DoesNotExistError if the image path is in fact invalid.
    Raises a SecurityError if the image path is outside of IMAGES_BASE_DIR.
    Raises a DBError if the database record cannot be created.
    """
    db_own = (_db_session is None)
    db_session = _db_session or data_manager.db_get_session()
    db_error = False
    try:
        # Get (or create) db record for the file
        on_create = on_image_db_create_anon_history if anon_history \
            else on_image_db_create
        db_image = data_manager.get_or_create_image(
            rel_path, on_create, _db_session=db_session
        )
        if not db_image:
            # Not expected
            raise DBError('Failed to add image to database: ' + rel_path)

        # Burst PDF if we need to
        # TODO This would be better in on_image_db_create if we can get a task_manager without
        #      importing the one from flask_app. Needs to be compatible with the task server.
        if burst_pdf and app.config['PDF_BURST_TO_PNG']:
            can_burst = get_file_extension(rel_path) in app.config['PDF_FILE_TYPES']
            if can_burst:
                if burst_pdf == 'auto':
                    burst_pdf = not path_exists(
                        get_burst_path(rel_path),
                        require_directory=True
                    )
                if burst_pdf:
                    burst_pdf_file(rel_path, task_manager)

        if db_image.status == Image.STATUS_ACTIVE:
            # The normal case
            return db_image
        else:
            # We need to undelete the database record
            db_image.status = Image.STATUS_ACTIVE
            if anon_history:
                on_image_db_create_anon_history(db_image)
            else:
                on_image_db_create(db_image)

            # Check whether the file's folder needs to be undeleted too
            if db_image.folder.status == Folder.STATUS_DELETED:
                auto_sync_existing_folder(
                    db_image.folder.path,
                    data_manager,
                    _db_session=db_session
                )
            return db_image
    except:
        db_error = True
        raise
    finally:
        if db_own:
            try:
                if db_error:
                    db_session.rollback()
                else:
                    db_session.commit()
            finally:
                db_session.close()