001/*
002 *  Copyright 2020 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.schedule;
017
018import java.io.IOException;
019import java.nio.file.DirectoryStream;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.time.Instant;
023
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.quartz.JobExecutionContext;
027
028import org.ametys.core.file.FileHelper;
029import org.ametys.core.schedule.progression.ContainerProgressionTracker;
030import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
031
032/**
033 * Abstract schedulable to delete files or folders.
034 */
035public abstract class AbstractDeleteFilesSchedulable extends AbstractStaticSchedulable
036{
037    /** File Helper */
038    protected FileHelper _fileHelper;
039    
040    @Override
041    public void service(ServiceManager manager) throws ServiceException
042    {
043        super.service(manager);
044        _fileHelper = (FileHelper) manager.lookup(FileHelper.ROLE);
045    }
046    
047    /**
048     * Get the needed values so the script can run
049     * @param context the context
050     * @return the configuration with needed values for deletion
051     */
052    protected abstract DeleteFilesConfiguration _getConfiguration(JobExecutionContext context);
053    
054    @Override
055    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
056    {
057        DeleteFilesConfiguration configuration = _getConfiguration(context);
058        
059        Path root = configuration.getRootPath();
060        if (root == null)
061        {
062            throw new IOException("The path is empty.");
063        }
064        
065        if (Files.exists(root))
066        {
067            DirectoryStream.Filter<Path> fileFilter = _createFileFilter(configuration);
068            
069            if (configuration.deleteRoot() || Files.isRegularFile(root))
070            {
071                _delete(root, configuration, fileFilter);
072            }
073            else
074            {
075                try (DirectoryStream<Path> entries = Files.newDirectoryStream(root))
076                {
077                    for (Path entry : entries)
078                    {
079                        _delete(entry, configuration, fileFilter);
080                    }
081                }
082            }
083        }
084    }
085    
086    /**
087     * Delete all files corresponding to the file filter into the file tree.
088     * @param path the path to delete (can be a file or a directory)
089     * @param configuration the configuration for deletion
090     * @param fileFilter the file filter to apply
091     * @throws IOException if an error occured while exploring or deleting files
092     */
093    protected void _delete(Path path, DeleteFilesConfiguration configuration, DirectoryStream.Filter<Path> fileFilter) throws IOException
094    {
095        _fileHelper.delete(path, fileFilter, configuration.deleteRecursively(), configuration.deleteEmptyFolders());
096    }
097    
098    /**
099     * Create the file filter to delete files.
100     * @param configuration The delete files configuration
101     * @return a file filter
102     */
103    protected DirectoryStream.Filter<Path> _createFileFilter(DeleteFilesConfiguration configuration)
104    {
105        return new DirectoryStream.Filter<>()
106        {
107            @Override
108            public boolean accept(Path entry) throws IOException
109            {
110                return Files.getLastModifiedTime(entry)
111                    .toInstant()
112                    .isBefore(configuration.getAgeLimit());
113            }
114        };
115    }
116    
117    /**
118     * Inner class to store configuration for deletion.
119     */
120    protected static class DeleteFilesConfiguration
121    {
122        private Path _rootPath;
123        private boolean _deleteRoot;
124        private boolean _deleteRecursively;
125        private boolean _deleteEmptyFolders;
126        private Instant _ageLimit;
127        
128        /**
129         * Constructor.
130         * Default values:
131         *  - ageLimit = now
132         *  - deleteRoot = false
133         *  - deleteRecursively = true
134         *  - deleteEmptyFolders = true
135         * @param rootPath The root path for deletion
136         */
137        public DeleteFilesConfiguration(Path rootPath)
138        {
139            _rootPath = rootPath;
140            _ageLimit = Instant.now();
141            _deleteRoot = false;
142            _deleteRecursively = true;
143            _deleteEmptyFolders = true;
144        }
145        
146        /**
147         * Get the root folder or file to delete
148         * @return a Directory or a file, that will be deleted/emptied
149         */
150        public Path getRootPath()
151        {
152            return _rootPath;
153        }
154        
155        /**
156         * Set to <code>true</code> to delete the root folder.
157         * @param deleteRoot <code>true</code> to delete the root folde
158         */
159        public void setDeleteRoot(boolean deleteRoot)
160        {
161            _deleteRoot = deleteRoot;
162        }
163        
164        /**
165         * Do the root needs to be deleted or emptied ?
166         * @return <code>true</code> to delete the root folder
167         */
168        public boolean deleteRoot()
169        {
170            return _deleteRoot;
171        }
172        
173        /**
174         * Set to <code>true</code> to delete files recursively.
175         * @param deleteRecursively <code>true</code> to delete files recursively
176         */
177        public void setDeleteRecursively(boolean deleteRecursively)
178        {
179            _deleteRecursively = deleteRecursively;
180        }
181        
182        /**
183         * Do the folders needs to be deleted recursively ?
184         * @return <code>true</code> to delete files recursively
185         */
186        public boolean deleteRecursively()
187        {
188            return _deleteRecursively;
189        }
190        
191        /**
192         * Set to <code>true</code> to delete empty folders.
193         * @param deleteEmptyFolders <code>true</code> to delete empty folders
194         */
195        public void setDeleteEmptyFolders(boolean deleteEmptyFolders)
196        {
197            _deleteEmptyFolders = deleteEmptyFolders;
198        }
199        
200        /**
201         * When a folder is empty, do it needs to be deleted ?
202         * @return <code>true</code> to delete empty folders
203         */
204        public boolean deleteEmptyFolders()
205        {
206            return _deleteEmptyFolders;
207        }
208        
209        /**
210         * Set the age limit of files to delete.
211         * @param ageLimit The age limit
212         */
213        public void setAgeLimit(Instant ageLimit)
214        {
215            _ageLimit = ageLimit;
216        }
217        
218        /**
219         * Only delete files older than this instant.
220         * @return instant, only older files will be deleted
221         */
222        public Instant getAgeLimit()
223        {
224            return _ageLimit;
225        }
226    }
227}