001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017 018package org.apache.logging.log4j.core.appender.rolling.action; 019 020import java.io.IOException; 021import java.nio.file.FileVisitor; 022import java.nio.file.Files; 023import java.nio.file.Path; 024import java.util.List; 025import java.util.Objects; 026 027import org.apache.logging.log4j.core.Core; 028import org.apache.logging.log4j.core.config.Configuration; 029import org.apache.logging.log4j.core.config.plugins.Plugin; 030import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 031import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; 032import org.apache.logging.log4j.core.config.plugins.PluginElement; 033import org.apache.logging.log4j.core.config.plugins.PluginFactory; 034import org.apache.logging.log4j.core.lookup.StrSubstitutor; 035 036/** 037 * Rollover or scheduled action for deleting old log files that are accepted by the specified PathFilters. 038 */ 039@Plugin(name = "Delete", category = Core.CATEGORY_NAME, printObject = true) 040public class DeleteAction extends AbstractPathAction { 041 042 private final PathSorter pathSorter; 043 private final boolean testMode; 044 private final ScriptCondition scriptCondition; 045 046 /** 047 * Creates a new DeleteAction that starts scanning for files to delete from the specified base path. 048 * 049 * @param basePath base path from where to start scanning for files to delete. 050 * @param followSymbolicLinks whether to follow symbolic links. Default is false. 051 * @param maxDepth The maxDepth parameter is the maximum number of levels of directories to visit. A value of 0 052 * means that only the starting file is visited, unless denied by the security manager. A value of 053 * MAX_VALUE may be used to indicate that all levels should be visited. 054 * @param testMode if true, files are not deleted but instead a message is printed to the <a 055 * href="http://logging.apache.org/log4j/2.x/manual/configuration.html#StatusMessages">status logger</a> 056 * at INFO level. Users can use this to do a dry run to test if their configuration works as expected. 057 * @param sorter sorts 058 * @param pathConditions an array of path filters (if more than one, they all need to accept a path before it is 059 * deleted). 060 * @param scriptCondition 061 */ 062 DeleteAction(final String basePath, final boolean followSymbolicLinks, final int maxDepth, final boolean testMode, 063 final PathSorter sorter, final PathCondition[] pathConditions, final ScriptCondition scriptCondition, 064 final StrSubstitutor subst) { 065 super(basePath, followSymbolicLinks, maxDepth, pathConditions, subst); 066 this.testMode = testMode; 067 this.pathSorter = Objects.requireNonNull(sorter, "sorter"); 068 this.scriptCondition = scriptCondition; 069 if (scriptCondition == null && (pathConditions == null || pathConditions.length == 0)) { 070 LOGGER.error("Missing Delete conditions: unconditional Delete not supported"); 071 throw new IllegalArgumentException("Unconditional Delete not supported"); 072 } 073 } 074 075 /* 076 * (non-Javadoc) 077 * 078 * @see org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute() 079 */ 080 @Override 081 public boolean execute() throws IOException { 082 return scriptCondition != null ? executeScript() : super.execute(); 083 } 084 085 private boolean executeScript() throws IOException { 086 final List<PathWithAttributes> selectedForDeletion = callScript(); 087 if (selectedForDeletion == null) { 088 LOGGER.trace("Script returned null list (no files to delete)"); 089 return true; 090 } 091 deleteSelectedFiles(selectedForDeletion); 092 return true; 093 } 094 095 private List<PathWithAttributes> callScript() throws IOException { 096 final List<PathWithAttributes> sortedPaths = getSortedPaths(); 097 trace("Sorted paths:", sortedPaths); 098 final List<PathWithAttributes> result = scriptCondition.selectFilesToDelete(getBasePath(), sortedPaths); 099 return result; 100 } 101 102 private void deleteSelectedFiles(final List<PathWithAttributes> selectedForDeletion) throws IOException { 103 trace("Paths the script selected for deletion:", selectedForDeletion); 104 for (final PathWithAttributes pathWithAttributes : selectedForDeletion) { 105 final Path path = pathWithAttributes == null ? null : pathWithAttributes.getPath(); 106 if (isTestMode()) { 107 LOGGER.info("Deleting {} (TEST MODE: file not actually deleted)", path); 108 } else { 109 delete(path); 110 } 111 } 112 } 113 114 /** 115 * Deletes the specified file. 116 * 117 * @param path the file to delete 118 * @throws IOException if a problem occurred deleting the file 119 */ 120 protected void delete(final Path path) throws IOException { 121 LOGGER.trace("Deleting {}", path); 122 Files.deleteIfExists(path); 123 } 124 125 /* 126 * (non-Javadoc) 127 * 128 * @see org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute(FileVisitor) 129 */ 130 @Override 131 public boolean execute(final FileVisitor<Path> visitor) throws IOException { 132 final List<PathWithAttributes> sortedPaths = getSortedPaths(); 133 trace("Sorted paths:", sortedPaths); 134 135 for (final PathWithAttributes element : sortedPaths) { 136 try { 137 visitor.visitFile(element.getPath(), element.getAttributes()); 138 } catch (final IOException ioex) { 139 LOGGER.error("Error in post-rollover Delete when visiting {}", element.getPath(), ioex); 140 visitor.visitFileFailed(element.getPath(), ioex); 141 } 142 } 143 // TODO return (visitor.success || ignoreProcessingFailure) 144 return true; // do not abort rollover even if processing failed 145 } 146 147 private void trace(final String label, final List<PathWithAttributes> sortedPaths) { 148 LOGGER.trace(label); 149 for (final PathWithAttributes pathWithAttributes : sortedPaths) { 150 LOGGER.trace(pathWithAttributes); 151 } 152 } 153 154 /** 155 * Returns a sorted list of all files up to maxDepth under the basePath. 156 * 157 * @return a sorted list of files 158 * @throws IOException 159 */ 160 List<PathWithAttributes> getSortedPaths() throws IOException { 161 final SortingVisitor sort = new SortingVisitor(pathSorter); 162 super.execute(sort); 163 final List<PathWithAttributes> sortedPaths = sort.getSortedPaths(); 164 return sortedPaths; 165 } 166 167 /** 168 * Returns {@code true} if files are not deleted even when all conditions accept a path, {@code false} otherwise. 169 * 170 * @return {@code true} if files are not deleted even when all conditions accept a path, {@code false} otherwise 171 */ 172 public boolean isTestMode() { 173 return testMode; 174 } 175 176 @Override 177 protected FileVisitor<Path> createFileVisitor(final Path visitorBaseDir, final List<PathCondition> conditions) { 178 return new DeletingVisitor(visitorBaseDir, conditions, testMode); 179 } 180 181 /** 182 * Create a DeleteAction. 183 * 184 * @param basePath base path from where to start scanning for files to delete. 185 * @param followLinks whether to follow symbolic links. Default is false. 186 * @param maxDepth The maxDepth parameter is the maximum number of levels of directories to visit. A value of 0 187 * means that only the starting file is visited, unless denied by the security manager. A value of 188 * MAX_VALUE may be used to indicate that all levels should be visited. 189 * @param testMode if true, files are not deleted but instead a message is printed to the <a 190 * href="http://logging.apache.org/log4j/2.x/manual/configuration.html#StatusMessages">status logger</a> 191 * at INFO level. Users can use this to do a dry run to test if their configuration works as expected. 192 * Default is false. 193 * @param PathSorter a plugin implementing the {@link PathSorter} interface 194 * @param PathConditions an array of path conditions (if more than one, they all need to accept a path before it is 195 * deleted). 196 * @param config The Configuration. 197 * @return A DeleteAction. 198 */ 199 @PluginFactory 200 public static DeleteAction createDeleteAction( 201 // @formatter:off 202 @PluginAttribute("basePath") final String basePath, 203 @PluginAttribute(value = "followLinks") final boolean followLinks, 204 @PluginAttribute(value = "maxDepth", defaultInt = 1) final int maxDepth, 205 @PluginAttribute(value = "testMode") final boolean testMode, 206 @PluginElement("PathSorter") final PathSorter sorterParameter, 207 @PluginElement("PathConditions") final PathCondition[] pathConditions, 208 @PluginElement("ScriptCondition") final ScriptCondition scriptCondition, 209 @PluginConfiguration final Configuration config) { 210 // @formatter:on 211 final PathSorter sorter = sorterParameter == null ? new PathSortByModificationTime(true) : sorterParameter; 212 return new DeleteAction(basePath, followLinks, maxDepth, testMode, sorter, pathConditions, scriptCondition, 213 config.getStrSubstitutor()); 214 } 215}