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