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
def _get_nearest_parent_folder(rel_path, data_manager, db_session): """ Returns the nearest active parent folder object for rel_path that already exists in the database. E.g. For rel_path a/b/c/d If the database contains all folders then c is returned. If the database contains only a and b so far then b is returned. If no folders in rel_path exist then the root folder is returned. Raises a DBError if no parent folder can be found at all (but this should never happen). """ # Convert a/b/c/d to ['a','b','c','d'] _, _, f_list = filepath_components(add_sep(rel_path)) # Start at root folder try_path = '' db_parent_folder = data_manager.get_folder( folder_path=try_path, _db_session=db_session ) # Then try to get more specific if len(f_list) > 1: # Loop to len-1 to stop at c in a/b/c/d for idx in range(len(f_list) - 1): try_path += (os.path.sep + f_list[idx]) db_f = data_manager.get_folder( folder_path=try_path, _db_session=db_session ) if db_f and db_f.status == Folder.STATUS_ACTIVE: db_parent_folder = db_f else: break # db_parent_folder should at least be the root folder if not db_parent_folder: raise DBError('Failed to identify a parent folder for: ' + rel_path) return db_parent_folder
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()