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}