/* * JBoss, Home of Professional Open Source * Copyright 2014, JBoss Inc., and individual contributors as indicated * by the @authors tag. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.as.patching.management; import org.jboss.as.patching.logging.PatchLogger; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.Collection; /** * Recursively removes files from a given directory. If any of the files couldn't be removed, it restores all deleted files. * * @author Bartosz Spyrko-Smietanko */ class DeleteOp { public static final String BACKUP_FOLDER = ".bkp"; private final File fileToDelete; private final FilenameFilter fileFilter; private final File backupRoot; DeleteOp(File toDelete, FilenameFilter fileFilter) { this.fileToDelete = toDelete; this.fileFilter = fileFilter; backupRoot = new File(toDelete.getParentFile(), BACKUP_FOLDER); } /** * remove files from directory. All-or-nothing operation - if any of the files fails to be removed, all deleted files are restored. * * The operation is performed in two steps - first all the files are moved to a backup folder, and afterwards backup folder is removed. * If an error occurs in the first step of the operation, all files are restored and the operation ends with status {@code false}. * If an error occurs in the second step, the operation ends with status {@code false}, but the files are not rolled back. * * @throws IOException if an error occurred */ public void execute() throws IOException { try { prepare(); boolean commitResult = commit(); if (commitResult == false) { throw PatchLogger.ROOT_LOGGER.failedToDeleteBackup(); } } catch (PrepareException pe){ rollback(); throw PatchLogger.ROOT_LOGGER.failedToDelete(pe.getPath()); } } /** * performs all delete operations together - if error occurs in one of them, all the operations are rolled back. * For details see {@link DeleteOp#execute()} * * @param deleteOps list of {@code DeleteOp} to perform * @throws IOException if an error occurred */ public static void execute(Collection<DeleteOp> deleteOps) throws IOException { try { deleteOps.forEach(DeleteOp::prepare); // best effort cleanup - delete what's possible and report error if anything remains boolean commitResult = true; for (DeleteOp op : deleteOps) { commitResult &= op.commit(); } if (!commitResult) { throw PatchLogger.ROOT_LOGGER.failedToDeleteBackup(); } } catch (PrepareException pe) { deleteOps.forEach(DeleteOp::rollback); throw PatchLogger.ROOT_LOGGER.failedToDelete(pe.getPath()); } } private void prepare() throws PrepareException { backupRoot.mkdir(); moveToBackup(fileToDelete, backupRoot, fileFilter); } private void moveToBackup(File file, File bkp, FilenameFilter fileFilter) throws PrepareException { if (!fileFilter.accept(file.getParentFile(), file.getName())) { // the file is ignored - do nothing return; } if (file.isDirectory()) { final File backupFolder = createBackupFolder(file, bkp); for (File child : file.listFiles(fileFilter)) { moveToBackup(child, backupFolder, fileFilter); } if (isEmptyFolder(file)) { boolean deleted = file.delete(); if (!deleted) { throw new PrepareException(file); } } } else { final File dest = new File(bkp, file.getName()); boolean moved = file.renameTo(dest); if (!moved) { throw new PrepareException(file); } } } private File createBackupFolder(File file, File bkp) throws PrepareException { final File backupFolder = new File(bkp, file.getName()); if (backupFolder.exists() || !backupFolder.mkdir()) { throw new PrepareException(file); } return backupFolder; } private static boolean isEmptyFolder(File file) { return file.list() != null && file.list().length == 0; } private boolean commit() { final File deleteRoot = new File(backupRoot, fileToDelete.getName()); boolean commitResult = true; if (deleteRoot.exists()) { commitResult = doDelete(deleteRoot); } if (isEmptyFolder(backupRoot)) { commitResult &= backupRoot.delete(); } return commitResult; } private boolean doDelete(File file) { boolean result = true; if (file.isDirectory()) { for (File child : file.listFiles()) { result &= doDelete(child); } } if (!file.delete()) { PatchLogger.ROOT_LOGGER.cannotDeleteFile(file.getAbsolutePath()); result = false; } return result; } private void rollback() { try { if (!backupRoot.exists()) { return; // it's OK - no files were originally backed up } final File source = new File(backupRoot, fileToDelete.getName()); doRollback(source, fileToDelete); if (isEmptyFolder(backupRoot)) { backupRoot.delete(); } } catch (RollbackException e) { PatchLogger.ROOT_LOGGER.deleteRollbackError(e.getPath(), e.getMessage()); } } private static void doRollback(File source, File destination) throws RollbackException { if (source.isDirectory()) { if (!destination.exists()) { destination.mkdir(); } else if (!destination.isDirectory()) { throw new RollbackException(source, "file with the same name exists"); } for (File child : source.listFiles()) { doRollback(child, new File(destination, child.getName())); } if (isEmptyFolder(source)) { if (!source.delete()) { throw new RollbackException(source, "unable to delete folder"); } } else { throw new RollbackException(source, "directory has unexpected files"); } } else { if (destination.exists()) { throw new RollbackException(source, "file with the same name already exists"); } if (!source.renameTo(destination)) { throw new RollbackException(source, "unable to move file"); } } } private static class PrepareException extends RuntimeException { private final String path; public PrepareException(File file) { this.path = file.getAbsolutePath(); } public String getPath() { return path; } } private static class RollbackException extends Exception { private String path; public RollbackException(File source, String msg) { super(msg); this.path = source.getAbsolutePath(); } public String getPath() { return path; } } }