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.time.ZonedDateTime; 027import java.util.Calendar; 028import java.util.Collection; 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.DateUtils; 055import org.ametys.core.util.FilenameUtils; 056import org.ametys.core.util.URIUtils; 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 uploadFile = new File(_getUploadDir(user != null ? UserIdentity.userIdentityToString(user) : null, id), FilenameUtils.encodeName(filename)); 201 202 if (_logger.isInfoEnabled()) 203 { 204 _logger.info("Using file: " + uploadFile); 205 } 206 207 if (!uploadFile.getParentFile().mkdirs()) 208 { 209 throw new IOException("Unable to create directory: " + uploadFile.getParent()); 210 } 211 212 try (OutputStream os = new FileOutputStream(uploadFile)) 213 { 214 IOUtils.copy(is, os); 215 } 216 217 return new FSUpload(_context, uploadFile); 218 } 219 220 @Override 221 public Upload getUpload(UserIdentity user, String id) throws NoSuchElementException 222 { 223 File uploadDir = _getUploadDir(user != null ? UserIdentity.userIdentityToString(user) : null, id); 224 225 if (_logger.isDebugEnabled()) 226 { 227 _logger.debug("Using directory: " + uploadDir); 228 } 229 230 if (!uploadDir.exists() || !uploadDir.isDirectory()) 231 { 232 throw new NoSuchElementException("No directory: " + uploadDir); 233 } 234 235 Collection<File> files = FileUtils.listFiles(uploadDir, TrueFileFilter.INSTANCE, FalseFileFilter.INSTANCE); 236 237 if (_logger.isInfoEnabled()) 238 { 239 _logger.info("Found files: " + files); 240 } 241 242 if (files.isEmpty()) 243 { 244 throw new NoSuchElementException("No files in directory: " + uploadDir); 245 } 246 247 // Use first file found 248 return new FSUpload(_context, files.iterator().next()); 249 } 250 251 @Override 252 public void dispose() 253 { 254 cancel(); 255 _timer.cancel(); 256 _globalUploadsDir = null; 257 _logger = null; 258 } 259 260 /** 261 * Retrieves the upload directory for a login and an upload id. 262 * @param login the login. 263 * @param id the upload id. 264 * @return the upload directory. 265 */ 266 protected File _getUploadDir(String login, String id) 267 { 268 String fileName = login != null ? FilenameUtils.encodeName(login) : "_Anonymous"; 269 return new File(new File(_globalUploadsDir, fileName), id); 270 } 271 272 /** 273 * {@link Upload} implementation on file system. 274 */ 275 protected static class FSUpload implements Upload 276 { 277 private org.apache.cocoon.environment.Context _context; 278 private File _file; 279 280 /** 281 * Creates a FSUpload from a file. 282 * @param context the context. 283 * @param file the file. 284 */ 285 public FSUpload(org.apache.cocoon.environment.Context context, File file) 286 { 287 _context = context; 288 _file = file; 289 } 290 291 @Override 292 public String getId() 293 { 294 return _file.getParentFile().getName(); 295 } 296 297 @Override 298 public ZonedDateTime getUploadedDate() 299 { 300 return DateUtils.asZonedDateTime(_file.lastModified()); 301 } 302 303 @Override 304 public String getFilename() 305 { 306 return URIUtils.decode(_file.getName()); 307 } 308 309 @Override 310 public String getMimeType() 311 { 312 String mimeType = _context.getMimeType(getFilename().toLowerCase()); 313 314 if (mimeType == null) 315 { 316 mimeType = "application/unknown"; 317 } 318 319 return mimeType; 320 } 321 322 @Override 323 public long getLength() 324 { 325 return _file.length(); 326 } 327 328 @Override 329 public InputStream getInputStream() 330 { 331 try 332 { 333 return new FileInputStream(_file); 334 } 335 catch (FileNotFoundException e) 336 { 337 throw new RuntimeException("Missing file: " + _file, e); 338 } 339 } 340 } 341}