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