001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.util.file; 018 019import java.io.BufferedInputStream; 020import java.io.BufferedOutputStream; 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FileOutputStream; 024import java.io.IOException; 025import java.io.InputStream; 026import java.net.URL; 027import java.nio.charset.StandardCharsets; 028import java.time.Instant; 029import org.apache.wicket.util.encoding.UrlDecoder; 030import org.apache.wicket.util.io.IOUtils; 031import org.apache.wicket.util.io.Streams; 032import org.apache.wicket.util.lang.Args; 033import org.apache.wicket.util.string.Strings; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037/** 038 * File utility methods. 039 * 040 * @author Jonathan Locke 041 */ 042public class Files 043{ 044 private static final Logger logger = LoggerFactory.getLogger(Files.class); 045 046 // protocols for urls 047 private static final String URL_FILE_PREFIX = "file:"; 048 private static final String URL_LOCAL_JAR_FILE_PREFIX = "jar:file:"; 049 050 // characters not allowed in filenames 051 private static final String FILENAME_FORBIDDEN_CHARACTERS = "\"*/:<>?\\|,"; 052 053 /** 054 * Private constructor to prevent instantiation. 055 */ 056 private Files() 057 { 058 } 059 060 /** 061 * Strips off the given extension (probably returned from Files.extension()) from the path, 062 * yielding a base pathname. 063 * 064 * @param path 065 * The path, possibly with an extension to strip 066 * @param extension 067 * The extension to strip, or null if no extension exists 068 * @return The path without any extension 069 */ 070 public static String basePath(final String path, final String extension) 071 { 072 if (extension != null) 073 { 074 return path.substring(0, path.length() - extension.length() - 1); 075 } 076 return path; 077 } 078 079 /** 080 * Gets extension from path 081 * 082 * @param path 083 * The path 084 * @return The extension, like "bmp" or "html", or null if none can be found 085 */ 086 public static String extension(final String path) 087 { 088 if (path.indexOf('.') != -1) 089 { 090 return Strings.lastPathComponent(path, '.'); 091 } 092 return null; 093 } 094 095 /** 096 * Gets filename from path 097 * 098 * @param path 099 * The path 100 * @return The filename 101 */ 102 public static String filename(final String path) 103 { 104 return Strings.lastPathComponent(path.replace('/', java.io.File.separatorChar), 105 java.io.File.separatorChar); 106 } 107 108 /** 109 * Deletes a normal file. 110 * <p> 111 * If the file cannot be deleted for any reason then at most 50 retries are attempted with delay 112 * of 100ms at each 10th attempt. 113 * 114 * @param file 115 * the file to delete 116 * @return {@code true} if file was deleted, {@code false} if the file don't exist, is a folder 117 * or cannot be removed for some reason 118 */ 119 public static boolean remove(final java.io.File file) 120 { 121 if (file != null && file.isFile()) 122 { 123 for (int j = 0; j < 5; ++j) 124 { 125 for (int i = 0; i < 10; ++i) 126 { 127 if (file.delete()) 128 { 129 return true; 130 } 131 } 132 try 133 { 134 Thread.sleep(100); 135 } 136 catch (InterruptedException ix) 137 { 138 Thread.currentThread().interrupt(); 139 } 140 } 141 } 142 143 return false; 144 } 145 146 /** 147 * Deletes a folder by recursively removing the files and folders inside it. Delegates the work 148 * to {@link #remove(File)} for plain files. 149 * 150 * @param folder 151 * the folder to delete 152 * @return {@code true} if the folder is deleted successfully. 153 */ 154 public static boolean removeFolder(final File folder) 155 { 156 if (folder == null) 157 { 158 return false; 159 } 160 161 if (folder.isDirectory()) 162 { 163 File[] files = folder.listFiles(); 164 if (files != null) 165 { 166 for (File file : files) 167 { 168 if (file.isDirectory()) 169 { 170 removeFolder(file); 171 } 172 else 173 { 174 remove(file); 175 } 176 } 177 } 178 } 179 180 // delete the empty folder 181 return folder.delete(); 182 } 183 184 /** 185 * Schedules a file for removal asynchronously. 186 * 187 * @param file 188 * the file to be removed 189 * @param fileCleaner 190 * the file cleaner that will be used to remove the file 191 * @return {@code false} if the {@code file} is <em>null</em> or a folder, {@code true} - 192 * otherwise (i.e. if it is scheduled) 193 */ 194 public static boolean removeAsync(final File file, final IFileCleaner fileCleaner) 195 { 196 if (file == null || file.isDirectory()) 197 { 198 return false; 199 } 200 201 Args.notNull(fileCleaner, "fileCleaner"); 202 203 fileCleaner.track(file, new Object()); 204 205 return true; 206 } 207 208 209 /** 210 * Schedules a folder and all files inside it for asynchronous removal. 211 * 212 * @param folder 213 * the folder to be removed 214 * @param fileCleaner 215 * the file cleaner that will be used to remove the file 216 * @return {@code false} if the {@code folder} is <em>null</em> or a normal file, {@code true} - 217 * otherwise (i.e. if it is scheduled) 218 */ 219 public static boolean removeFolderAsync(final File folder, final IFileCleaner fileCleaner) 220 { 221 if (folder == null || folder.isFile()) 222 { 223 return false; 224 } 225 226 Args.notNull(fileCleaner, "fileCleaner"); 227 228 fileCleaner.track(folder, new Object(), new FolderDeleteStrategy()); 229 230 return true; 231 } 232 233 /** 234 * Writes the given input stream to the given file 235 * 236 * @param file 237 * The file to write to 238 * @param input 239 * The input 240 * @return Number of bytes written 241 * @throws IOException 242 */ 243 public static int writeTo(final java.io.File file, final InputStream input) 244 throws IOException 245 { 246 return writeTo(file, input, 4096); 247 } 248 249 /** 250 * read binary file fully 251 * 252 * @param file 253 * file to read 254 * @return byte array representing the content of the file 255 * @throws IOException 256 * is something went wrong 257 */ 258 public static byte[] readBytes(final File file) throws IOException 259 { 260 FileInputStream stream = new FileInputStream(file); 261 262 try 263 { 264 return IOUtils.toByteArray(stream); 265 } 266 finally 267 { 268 stream.close(); 269 } 270 } 271 272 /** 273 * Writes the given input stream to the given file 274 * 275 * @param file 276 * The file to write to 277 * @param input 278 * The input 279 * @param bufSize 280 * The memory buffer size. 4096 is a good value. 281 * @return Number of bytes written 282 * @throws IOException 283 */ 284 public static int writeTo(final java.io.File file, final InputStream input, 285 final int bufSize) throws IOException 286 { 287 final FileOutputStream out = new FileOutputStream(file); 288 try 289 { 290 return Streams.copy(input, out, bufSize); 291 } 292 finally 293 { 294 out.close(); 295 } 296 } 297 298 /** 299 * <p> 300 * Replaces commonly unsupported characters with '_' 301 * </p> 302 * 303 * @param filename 304 * to be cleaned 305 * @return cleaned filename 306 */ 307 public static String cleanupFilename(final String filename) 308 { 309 String name = filename; 310 for (int i = 0; i < FILENAME_FORBIDDEN_CHARACTERS.length(); i++) 311 { 312 name = name.replace(FILENAME_FORBIDDEN_CHARACTERS.charAt(i), '_'); 313 } 314 return name; 315 } 316 317 /** 318 * make a copy of a file 319 * 320 * @param sourceFile 321 * source file that needs to be cloned 322 * @param targetFile 323 * target file that should be a duplicate of source file 324 * @throws IOException 325 * if something went wrong 326 */ 327 public static void copy(final File sourceFile, final File targetFile) throws IOException 328 { 329 BufferedInputStream in = null; 330 BufferedOutputStream out = null; 331 332 try 333 { 334 in = new BufferedInputStream(new FileInputStream(sourceFile)); 335 out = new BufferedOutputStream(new FileOutputStream(targetFile)); 336 337 IOUtils.copy(in, out); 338 } 339 finally 340 { 341 try 342 { 343 IOUtils.close(in); 344 345 } 346 finally 347 { 348 IOUtils.close(out); 349 } 350 } 351 } 352 353 /** 354 * for urls that point to local files (e.g. 'file:' or 'jar:file:') this methods returns a 355 * reference to the local file 356 * 357 * @param url 358 * url of the resource 359 * 360 * @return reference to a local file if url contains one, <code>null</code> otherwise 361 * 362 * @see #getLocalFileFromUrl(String) 363 */ 364 public static File getLocalFileFromUrl(URL url) 365 { 366 final URL location = Args.notNull(url, "url"); 367 return getLocalFileFromUrl(UrlDecoder.PATH_INSTANCE.decode(location.toExternalForm(), StandardCharsets.UTF_8)); 368 } 369 370 /** 371 * for urls that point to local files (e.g. 'file:' or 'jar:file:') this methods returns a 372 * reference to the local file 373 * 374 * @param url 375 * url of the resource 376 * 377 * @return reference to a local file if url contains one, <code>null</code> otherwise 378 * 379 * @see #getLocalFileFromUrl(URL) 380 */ 381 public static File getLocalFileFromUrl(String url) 382 { 383 final String location = Args.notNull(url, "url"); 384 385 // check for 'file:' 386 if (location.startsWith(URL_FILE_PREFIX)) 387 { 388 return new File(location.substring(URL_FILE_PREFIX.length())); 389 } 390 // check for 'jar:file:' 391 else if (location.startsWith(URL_LOCAL_JAR_FILE_PREFIX)) 392 { 393 final String path = location.substring(URL_LOCAL_JAR_FILE_PREFIX.length()); 394 final int resourceAt = path.indexOf('!'); 395 396 // for jar:file: the '!' is mandatory 397 if (resourceAt == -1) 398 { 399 return null; 400 } 401 return new File(path.substring(0, resourceAt)); 402 } 403 else 404 { 405 return null; 406 } 407 } 408 409 /** 410 * get last modification timestamp for file 411 * 412 * @param file 413 * 414 * @return timestamp 415 */ 416 public static Instant getLastModified(File file) 417 { 418 // get file modification timestamp 419 long millis = file.lastModified(); 420 421 // zero indicates the timestamp could not be retrieved or the file does not exist 422 if (millis == 0) 423 { 424 return null; 425 } 426 427 // last file modification timestamp 428 return Instant.ofEpochMilli(millis); 429 } 430 431 /** 432 * Utility method for creating a directory. If the creation didn't succeed for some reason then 433 * at most 50 attempts are made with delay of 100ms at every 10th attempt. 434 * 435 * @param folder 436 * the folder to create 437 * @return {@code true} if the creation is successful, {@code false} - otherwise 438 */ 439 public static boolean mkdirs(File folder) 440 { 441 // for some reason, simple file.mkdirs sometimes fails under heavy load 442 for (int j = 0; j < 5; ++j) 443 { 444 for (int i = 0; i < 10; ++i) 445 { 446 if (folder.mkdirs()) 447 { 448 return true; 449 } 450 } 451 try 452 { 453 Thread.sleep(100); 454 if (folder.exists()) return true; 455 } 456 catch (InterruptedException ix) 457 { 458 Thread.currentThread().interrupt(); 459 } 460 } 461 logger.error("Failed to create directory: " + folder); 462 return false; 463 } 464 465 /** 466 * List all files inside the given file. 467 * 468 * @param file directory 469 * @return files, never {@code null} 470 */ 471 public static File[] list(File file) { 472 File[] files = file.listFiles(); 473 if (files == null) { 474 files = new File[0]; 475 } 476 return files; 477 } 478}