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="/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="/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}