001/*
002 *  Copyright 2012 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 */
016
017package org.ametys.plugins.core.impl.upload;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileNotFoundException;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.util.Calendar;
027import java.util.Collection;
028import java.util.Date;
029import java.util.GregorianCalendar;
030import java.util.NoSuchElementException;
031import java.util.Timer;
032import java.util.TimerTask;
033import java.util.UUID;
034
035import org.apache.avalon.framework.activity.Disposable;
036import org.apache.avalon.framework.activity.Initializable;
037import org.apache.avalon.framework.context.Context;
038import org.apache.avalon.framework.context.ContextException;
039import org.apache.avalon.framework.context.Contextualizable;
040import org.apache.avalon.framework.logger.LogEnabled;
041import org.apache.avalon.framework.logger.Logger;
042import org.apache.avalon.framework.thread.ThreadSafe;
043import org.apache.cocoon.Constants;
044import org.apache.commons.io.FileUtils;
045import org.apache.commons.io.IOUtils;
046import org.apache.commons.io.filefilter.AbstractFileFilter;
047import org.apache.commons.io.filefilter.DirectoryFileFilter;
048import org.apache.commons.io.filefilter.FalseFileFilter;
049import org.apache.commons.io.filefilter.TrueFileFilter;
050
051import org.ametys.core.upload.Upload;
052import org.ametys.core.upload.UploadManager;
053import org.ametys.core.user.UserIdentity;
054import org.ametys.core.util.FilenameUtils;
055import org.ametys.core.util.URIUtils;
056import org.ametys.runtime.util.AmetysHomeHelper;
057
058/**
059 * {@link UploadManager} which stores uploaded files into the
060 * <code>uploads-user</code> directory located in Ametys home
061 * <p>
062 * Note that this implementation is not cluster safe.
063 */
064public class FSUploadManager extends TimerTask implements UploadManager, ThreadSafe, Initializable, Contextualizable, LogEnabled, Disposable
065{
066    /**
067     * The path to the global uploads directory relative to ametys home
068     */
069    public static final String UPLOADS_DIRECTORY = "uploads-user";
070
071    /** Context. */
072    protected org.apache.cocoon.environment.Context _context;
073    /** Global uploads directory. */
074    protected File _globalUploadsDir;
075    /** Timer. */
076    protected Timer _timer;
077    /** Logger available to subclasses. */
078    private Logger _logger;
079    
080    @Override
081    public void enableLogging(Logger logger)
082    {
083        _logger = logger;
084    }
085    
086    @Override
087    public void contextualize(Context context) throws ContextException
088    {
089        // Retrieve context-root from context
090        _context = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
091    }
092    
093    @Override
094    public void initialize() throws Exception
095    {
096        if (_logger.isDebugEnabled())
097        {
098            _logger.debug("Starting timer");
099        }
100        
101        _globalUploadsDir = new File(AmetysHomeHelper.getAmetysHomeData(), UPLOADS_DIRECTORY);
102        
103        // Daemon thread
104        _timer = new Timer("FSUploadManager", true);
105        
106        // Start in 15 minutes and refresh each 24 hours
107        _timer.scheduleAtFixedRate(this, 15 * 60 * 1000, 24 * 60 * 60 * 1000);
108    }
109    
110    @Override
111    public void run()
112    {
113        if (_logger.isInfoEnabled())
114        {
115            _logger.info("Time to clean old uploads");
116        }
117        
118        try
119        {
120            Calendar calendar = new GregorianCalendar();
121            calendar.add(Calendar.DAY_OF_YEAR, -1);
122            final long yesterday = calendar.getTimeInMillis();
123            
124            // Retrieve login directories
125            String[] loginDirs = _globalUploadsDir.list(DirectoryFileFilter.INSTANCE);
126            
127            if (loginDirs != null)
128            {
129                for (String loginDir : loginDirs)
130                {
131                    File effectiveLoginDir = new File(_globalUploadsDir, loginDir);
132                    // Compute upload directories to remove
133                    String[] dirsToRemove = effectiveLoginDir.list(new AbstractFileFilter()
134                    {
135                        @Override
136                        public boolean accept(File file)
137                        {
138                            if (!file.isDirectory())
139                            {
140                                return false;
141                            }
142                            
143                            Collection<File> uploadFiles = FileUtils.listFiles(file, TrueFileFilter.INSTANCE, null);
144
145                            if (uploadFiles.isEmpty())
146                            {
147                                // Remove empty directory
148                                return true;
149                            }
150                            else
151                            {
152                                File firstFile = uploadFiles.iterator().next();
153    
154                                // Remove directory if the first file is one day old
155                                return firstFile.lastModified() < yesterday;
156                            }
157                        }
158                    });
159                    
160                    if (dirsToRemove != null)
161                    {
162                        for (String dirToRemove : dirsToRemove)
163                        {
164                            File uploadDir = new File(effectiveLoginDir, dirToRemove);
165                            
166                            if (_logger.isDebugEnabled())
167                            {
168                                _logger.debug("Removing directory: " + uploadDir);
169                            }
170            
171                            FileUtils.deleteDirectory(uploadDir);
172                        }
173                    }
174                }
175            }
176        }
177        catch (Exception e)
178        {
179            _logger.error("Unable to clean old uploads", e);
180        }
181    }
182    
183    @Override
184    public Upload storeUpload(UserIdentity user, String filename, InputStream is) throws IOException
185    {
186        if (!_globalUploadsDir.exists())
187        {
188            if (!_globalUploadsDir.mkdirs())
189            {
190                throw new IOException("Unable to create directory: " + _globalUploadsDir);
191            }
192        }
193        
194        // Create unique id
195        String id = UUID.randomUUID().toString();
196        
197        File uploadFile = null;
198
199        uploadFile = new File(_getUploadDir(user != null ? UserIdentity.userIdentityToString(user) : null, id), FilenameUtils.encodeName(filename));
200        
201        if (_logger.isInfoEnabled())
202        {
203            _logger.info("Using file: " + uploadFile);
204        }
205        
206        if (!uploadFile.getParentFile().mkdirs())
207        {
208            throw new IOException("Unable to create directory: " + uploadFile.getParent());
209        }
210        
211        try (OutputStream os = new FileOutputStream(uploadFile))
212        {
213            IOUtils.copy(is, os);
214        }
215        
216        return new FSUpload(_context, uploadFile);
217    }
218    
219    @Override
220    public Upload getUpload(UserIdentity user, String id) throws NoSuchElementException
221    {
222        File uploadDir = _getUploadDir(user != null ? UserIdentity.userIdentityToString(user) : null, id);
223        
224        if (_logger.isDebugEnabled())
225        {
226            _logger.debug("Using directory: " + uploadDir);
227        }
228        
229        if (!uploadDir.exists() || !uploadDir.isDirectory())
230        {
231            throw new NoSuchElementException("No directory: " + uploadDir);
232        }
233        
234        Collection<File> files = FileUtils.listFiles(uploadDir, TrueFileFilter.INSTANCE, FalseFileFilter.INSTANCE);
235        
236        if (_logger.isInfoEnabled())
237        {
238            _logger.info("Found files: " + files);
239        }
240        
241        if (files.isEmpty())
242        {
243            throw new NoSuchElementException("No files in directory: " + uploadDir);
244        }
245        
246        // Use first file found
247        return new FSUpload(_context, files.iterator().next());
248    }
249    
250    @Override
251    public void dispose()
252    {
253        cancel();
254        _timer.cancel();
255        _globalUploadsDir = null;
256        _logger = null;
257    }
258    
259    /**
260     * Retrieves the upload directory for a login and an upload id.
261     * @param login the login.
262     * @param id the upload id.
263     * @return the upload directory.
264     */
265    protected File _getUploadDir(String login, String id)
266    {
267        String fileName = login != null ? FilenameUtils.encodeName(login) : "_Anonymous";
268        return new File(new File(_globalUploadsDir, fileName), id);
269    }
270    
271    /**
272     * {@link Upload} implementation on file system.
273     */
274    protected static class FSUpload implements Upload
275    {
276        private org.apache.cocoon.environment.Context _context;
277        private File _file;
278        
279        /**
280         * Creates a FSUpload from a file.
281         * @param context the context.
282         * @param file the file.
283         */
284        public FSUpload(org.apache.cocoon.environment.Context context, File file)
285        {
286            _context = context;
287            _file = file;
288        }
289
290        @Override
291        public String getId()
292        {
293            return _file.getParentFile().getName();
294        }
295        
296        @Override
297        public Date getUploadedDate()
298        {
299            return new Date(_file.lastModified());
300        }
301        
302        @Override
303        public String getFilename()
304        {
305            return URIUtils.decode(_file.getName());
306        }
307        
308        @Override
309        public String getMimeType()
310        {
311            String mimeType = _context.getMimeType(getFilename().toLowerCase());
312            
313            if (mimeType == null)
314            {
315                mimeType = "application/unknown";
316            }
317            
318            return mimeType;
319        }
320        
321        @Override
322        public long getLength()
323        {
324            return _file.length();
325        }
326        
327        @Override
328        public InputStream getInputStream()
329        {
330            try
331            {
332                return new FileInputStream(_file);
333            }
334            catch (FileNotFoundException e)
335            {
336                throw new RuntimeException("Missing file: " + _file, e);
337            }
338        }
339    }
340}