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.FilenameFilter;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.time.ZonedDateTime;
028import java.util.Collection;
029import java.util.NoSuchElementException;
030import java.util.UUID;
031
032import org.apache.avalon.framework.activity.Initializable;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.logger.LogEnabled;
037import org.apache.avalon.framework.logger.Logger;
038import org.apache.avalon.framework.thread.ThreadSafe;
039import org.apache.cocoon.Constants;
040import org.apache.commons.io.FileUtils;
041import org.apache.commons.io.IOUtils;
042import org.apache.commons.io.filefilter.DirectoryFileFilter;
043import org.apache.commons.io.filefilter.FalseFileFilter;
044import org.apache.commons.io.filefilter.TrueFileFilter;
045
046import org.ametys.core.upload.Upload;
047import org.ametys.core.upload.UploadManager;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.util.DateUtils;
050import org.ametys.core.util.FilenameUtils;
051import org.ametys.core.util.URIUtils;
052import org.ametys.runtime.util.AmetysHomeHelper;
053
054/**
055 * {@link UploadManager} which stores uploaded files into the
056 * <code>uploads-user</code> directory located in Ametys home
057 * <p>
058 * Note that this implementation is not cluster safe.
059 */
060public class FSUploadManager implements UploadManager, ThreadSafe, Initializable, LogEnabled, Contextualizable
061{
062    /**
063     * The path to the global uploads directory relative to ametys home
064     */
065    public static final String UPLOADS_DIRECTORY = "uploads-user";
066
067    /** Context. */
068    protected org.apache.cocoon.environment.Context _cocoonContext;
069    /** Global uploads directory. */
070    protected File _globalUploadsDir;
071    /** Logger available to subclasses. */
072    private Logger _logger;
073
074    @Override
075    public void contextualize(Context context) throws ContextException
076    {
077        // Retrieve context-root from context
078        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
079    }
080    
081    @Override
082    public void enableLogging(Logger logger)
083    {
084        _logger = logger;
085    }
086        
087    @Override
088    public void initialize() throws Exception
089    {
090        _globalUploadsDir = new File(AmetysHomeHelper.getAmetysHomeData(), UPLOADS_DIRECTORY);
091    }
092    
093    @Override
094    public Upload storeUpload(UserIdentity user, String filename, InputStream is) throws IOException
095    {
096        if (!_globalUploadsDir.exists())
097        {
098            if (!_globalUploadsDir.mkdirs())
099            {
100                throw new IOException("Unable to create directory: " + _globalUploadsDir);
101            }
102        }
103        
104        // Create unique id
105        String id = UUID.randomUUID().toString();
106        
107        File uploadFile = null;
108
109        uploadFile = new File(_getUploadDir(user != null ? UserIdentity.userIdentityToString(user) : null, id), FilenameUtils.encodeName(filename));
110        
111        if (_logger.isInfoEnabled())
112        {
113            _logger.info("Using file: " + uploadFile);
114        }
115        
116        if (!uploadFile.getParentFile().mkdirs())
117        {
118            throw new IOException("Unable to create directory: " + uploadFile.getParent());
119        }
120        
121        try (OutputStream os = new FileOutputStream(uploadFile))
122        {
123            IOUtils.copy(is, os);
124        }
125        
126        return new FSUpload(_cocoonContext, uploadFile);
127    }
128    
129    @Override
130    public Upload getUpload(UserIdentity user, String id) throws NoSuchElementException
131    {
132        File uploadDir = _getUploadDir(user != null ? UserIdentity.userIdentityToString(user) : null, id);
133        
134        if (_logger.isDebugEnabled())
135        {
136            _logger.debug("Using directory: " + uploadDir);
137        }
138        
139        if (!uploadDir.exists() || !uploadDir.isDirectory())
140        {
141            throw new NoSuchElementException("No directory: " + uploadDir);
142        }
143        
144        Collection<File> files = FileUtils.listFiles(uploadDir, TrueFileFilter.INSTANCE, FalseFileFilter.INSTANCE);
145        
146        if (_logger.isInfoEnabled())
147        {
148            _logger.info("Found files: " + files);
149        }
150        
151        if (files.isEmpty())
152        {
153            throw new NoSuchElementException("No files in directory: " + uploadDir);
154        }
155        
156        // Use first file found
157        return new FSUpload(_cocoonContext, files.iterator().next());
158    }
159    
160    /**
161     * Retrieves the upload directory for a login and an upload id.
162     * @param login the login.
163     * @param id the upload id.
164     * @return the upload directory.
165     */
166    protected File _getUploadDir(String login, String id)
167    {
168        String fileName = login != null ? FilenameUtils.encodeName(login) : "_Anonymous";
169        return new File(new File(_globalUploadsDir, fileName), id);
170    }
171    
172    /**
173     * {@link Upload} implementation on file system.
174     */
175    protected static class FSUpload implements Upload
176    {
177        private org.apache.cocoon.environment.Context _context;
178        private File _file;
179        
180        /**
181         * Creates a FSUpload from a file.
182         * @param context the context.
183         * @param file the file.
184         */
185        public FSUpload(org.apache.cocoon.environment.Context context, File file)
186        {
187            _context = context;
188            _file = file;
189        }
190
191        @Override
192        public String getId()
193        {
194            return _file.getParentFile().getName();
195        }
196        
197        @Override
198        public ZonedDateTime getUploadedDate()
199        {
200            return DateUtils.asZonedDateTime(_file.lastModified());
201        }
202        
203        @Override
204        public String getFilename()
205        {
206            return URIUtils.decode(_file.getName());
207        }
208        
209        @Override
210        public String getMimeType()
211        {
212            String mimeType = _context.getMimeType(getFilename().toLowerCase());
213            
214            if (mimeType == null)
215            {
216                mimeType = "application/unknown";
217            }
218            
219            return mimeType;
220        }
221        
222        @Override
223        public long getLength()
224        {
225            return _file.length();
226        }
227        
228        @Override
229        public InputStream getInputStream()
230        {
231            try
232            {
233                return new FileInputStream(_file);
234            }
235            catch (FileNotFoundException e)
236            {
237                throw new RuntimeException("Missing file: " + _file, e);
238            }
239        }
240    }
241    
242    /**
243     * get directories under upload-user directory
244     * @return the directories
245     */
246    public File[] getUploadsDirectories()
247    {
248        return _globalUploadsDir.listFiles((FilenameFilter) DirectoryFileFilter.INSTANCE);
249    }
250}