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.camel.util; 018 019import java.io.File; 020import java.io.IOException; 021import java.nio.file.Files; 022import java.nio.file.StandardCopyOption; 023import java.util.ArrayDeque; 024import java.util.Deque; 025import java.util.Iterator; 026import java.util.Locale; 027import java.util.Random; 028 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031 032/** 033 * File utilities. 034 */ 035public final class FileUtil { 036 037 public static final int BUFFER_SIZE = 128 * 1024; 038 039 private static final Logger LOG = LoggerFactory.getLogger(FileUtil.class); 040 private static final int RETRY_SLEEP_MILLIS = 10; 041 /** 042 * The System property key for the user directory. 043 */ 044 private static final String USER_DIR_KEY = "user.dir"; 045 private static final File USER_DIR = new File(System.getProperty(USER_DIR_KEY)); 046 private static File defaultTempDir; 047 private static Thread shutdownHook; 048 private static boolean windowsOs = initWindowsOs(); 049 050 private FileUtil() { 051 // Utils method 052 } 053 054 private static boolean initWindowsOs() { 055 // initialize once as System.getProperty is not fast 056 String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); 057 return osName.contains("windows"); 058 } 059 060 public static File getUserDir() { 061 return USER_DIR; 062 } 063 064 /** 065 * Normalizes the path to cater for Windows and other platforms 066 */ 067 public static String normalizePath(String path) { 068 if (path == null) { 069 return null; 070 } 071 072 if (isWindows()) { 073 // special handling for Windows where we need to convert / to \\ 074 return path.replace('/', '\\'); 075 } else { 076 // for other systems make sure we use / as separators 077 return path.replace('\\', '/'); 078 } 079 } 080 081 /** 082 * Returns true, if the OS is windows 083 */ 084 public static boolean isWindows() { 085 return windowsOs; 086 } 087 088 @Deprecated 089 public static File createTempFile(String prefix, String suffix) throws IOException { 090 return createTempFile(prefix, suffix, null); 091 } 092 093 public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException { 094 // TODO: parentDir should be mandatory 095 File parent = (parentDir == null) ? getDefaultTempDir() : parentDir; 096 097 if (suffix == null) { 098 suffix = ".tmp"; 099 } 100 if (prefix == null) { 101 prefix = "camel"; 102 } else if (prefix.length() < 3) { 103 prefix = prefix + "camel"; 104 } 105 106 // create parent folder 107 parent.mkdirs(); 108 109 return Files.createTempFile(parent.toPath(), prefix, suffix).toFile(); 110 } 111 112 /** 113 * Strip any leading separators 114 */ 115 public static String stripLeadingSeparator(String name) { 116 if (name == null) { 117 return null; 118 } 119 while (name.startsWith("/") || name.startsWith(File.separator)) { 120 name = name.substring(1); 121 } 122 return name; 123 } 124 125 /** 126 * Does the name start with a leading separator 127 */ 128 public static boolean hasLeadingSeparator(String name) { 129 if (name == null) { 130 return false; 131 } 132 if (name.startsWith("/") || name.startsWith(File.separator)) { 133 return true; 134 } 135 return false; 136 } 137 138 /** 139 * Strip first leading separator 140 */ 141 public static String stripFirstLeadingSeparator(String name) { 142 if (name == null) { 143 return null; 144 } 145 if (name.startsWith("/") || name.startsWith(File.separator)) { 146 name = name.substring(1); 147 } 148 return name; 149 } 150 151 /** 152 * Strip any trailing separators 153 */ 154 public static String stripTrailingSeparator(String name) { 155 if (ObjectHelper.isEmpty(name)) { 156 return name; 157 } 158 159 String s = name; 160 161 // there must be some leading text, as we should only remove trailing separators 162 while (s.endsWith("/") || s.endsWith(File.separator)) { 163 s = s.substring(0, s.length() - 1); 164 } 165 166 // if the string is empty, that means there was only trailing slashes, and no leading text 167 // and so we should then return the original name as is 168 if (ObjectHelper.isEmpty(s)) { 169 return name; 170 } else { 171 // return without trailing slashes 172 return s; 173 } 174 } 175 176 /** 177 * Strips any leading paths 178 */ 179 public static String stripPath(String name) { 180 if (name == null) { 181 return null; 182 } 183 int posUnix = name.lastIndexOf('/'); 184 int posWin = name.lastIndexOf('\\'); 185 int pos = Math.max(posUnix, posWin); 186 187 if (pos != -1) { 188 return name.substring(pos + 1); 189 } 190 return name; 191 } 192 193 public static String stripExt(String name) { 194 return stripExt(name, false); 195 } 196 197 public static String stripExt(String name, boolean singleMode) { 198 if (name == null) { 199 return null; 200 } 201 202 // the name may have a leading path 203 int posUnix = name.lastIndexOf('/'); 204 int posWin = name.lastIndexOf('\\'); 205 int pos = Math.max(posUnix, posWin); 206 207 if (pos > 0) { 208 String onlyName = name.substring(pos + 1); 209 int pos2 = singleMode ? onlyName.lastIndexOf('.') : onlyName.indexOf('.'); 210 if (pos2 > 0) { 211 return name.substring(0, pos + pos2 + 1); 212 } 213 } else { 214 // if single ext mode, then only return last extension 215 int pos2 = singleMode ? name.lastIndexOf('.') : name.indexOf('.'); 216 if (pos2 > 0) { 217 return name.substring(0, pos2); 218 } 219 } 220 221 return name; 222 } 223 224 public static String onlyExt(String name) { 225 return onlyExt(name, false); 226 } 227 228 public static String onlyExt(String name, boolean singleMode) { 229 if (name == null) { 230 return null; 231 } 232 name = stripPath(name); 233 234 // extension is the first dot, as a file may have double extension such as .tar.gz 235 // if single ext mode, then only return last extension 236 int pos = singleMode ? name.lastIndexOf('.') : name.indexOf('.'); 237 if (pos != -1) { 238 return name.substring(pos + 1); 239 } 240 return null; 241 } 242 243 /** 244 * Returns only the leading path (returns <tt>null</tt> if no path) 245 */ 246 public static String onlyPath(String name) { 247 if (name == null) { 248 return null; 249 } 250 251 int posUnix = name.lastIndexOf('/'); 252 int posWin = name.lastIndexOf('\\'); 253 int pos = Math.max(posUnix, posWin); 254 255 if (pos > 0) { 256 return name.substring(0, pos); 257 } else if (pos == 0) { 258 // name is in the root path, so extract the path as the first char 259 return name.substring(0, 1); 260 } 261 // no path in name 262 return null; 263 } 264 265 /** 266 * Compacts a path by stacking it and reducing <tt>..</tt>, 267 * and uses OS specific file separators (eg {@link java.io.File#separator}). 268 */ 269 public static String compactPath(String path) { 270 return compactPath(path, "" + File.separatorChar); 271 } 272 273 /** 274 * Compacts a path by stacking it and reducing <tt>..</tt>, 275 * and uses the given separator. 276 * 277 */ 278 public static String compactPath(String path, char separator) { 279 return compactPath(path, "" + separator); 280 } 281 282 /** 283 * Compacts a file path by stacking it and reducing <tt>..</tt>, 284 * and uses the given separator. 285 */ 286 public static String compactPath(String path, String separator) { 287 if (path == null) { 288 return null; 289 } 290 291 if (path.startsWith("http:")) { 292 return path; 293 } 294 295 // only normalize if contains a path separator 296 if (path.indexOf('/') == -1 && path.indexOf('\\') == -1) { 297 return path; 298 } 299 300 // need to normalize path before compacting 301 path = normalizePath(path); 302 303 // preserve ending slash if given in input path 304 boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\"); 305 306 // preserve starting slash if given in input path 307 int cntSlashsAtStart = 0; 308 if (path.startsWith("/") || path.startsWith("\\")) { 309 cntSlashsAtStart++; 310 // for Windows, preserve up to 2 starting slashes, which is necessary for UNC paths. 311 if (isWindows() && path.length() > 1 && (path.charAt(1) == '/' || path.charAt(1) == '\\')) { 312 cntSlashsAtStart++; 313 } 314 } 315 316 Deque<String> stack = new ArrayDeque<>(); 317 318 // separator can either be windows or unix style 319 String separatorRegex = "\\\\|/"; 320 String[] parts = path.split(separatorRegex); 321 for (String part : parts) { 322 if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) { 323 // only pop if there is a previous path, which is not a ".." path either 324 stack.pop(); 325 } else if (part.equals(".") || part.isEmpty()) { 326 // do nothing because we don't want a path like foo/./bar or foo//bar 327 } else { 328 stack.push(part); 329 } 330 } 331 332 // build path based on stack 333 StringBuilder sb = new StringBuilder(); 334 335 for (int i = 0; i < cntSlashsAtStart; i++) { 336 sb.append(separator); 337 } 338 339 // now we build back using FIFO so need to use descending 340 for (Iterator<String> it = stack.descendingIterator(); it.hasNext();) { 341 sb.append(it.next()); 342 if (it.hasNext()) { 343 sb.append(separator); 344 } 345 } 346 347 if (endsWithSlash && stack.size() > 0) { 348 sb.append(separator); 349 } 350 351 return sb.toString(); 352 } 353 354 @Deprecated 355 private static synchronized File getDefaultTempDir() { 356 if (defaultTempDir != null && defaultTempDir.exists()) { 357 return defaultTempDir; 358 } 359 360 defaultTempDir = createNewTempDir(); 361 362 // create shutdown hook to remove the temp dir 363 shutdownHook = new Thread() { 364 @Override 365 public void run() { 366 removeDir(defaultTempDir); 367 } 368 }; 369 Runtime.getRuntime().addShutdownHook(shutdownHook); 370 371 return defaultTempDir; 372 } 373 374 /** 375 * Creates a new temporary directory in the <tt>java.io.tmpdir</tt> directory. 376 */ 377 @Deprecated 378 private static File createNewTempDir() { 379 String s = System.getProperty("java.io.tmpdir"); 380 File checkExists = new File(s); 381 if (!checkExists.exists()) { 382 throw new RuntimeException("The directory " 383 + checkExists.getAbsolutePath() 384 + " does not exist, please set java.io.tempdir" 385 + " to an existing directory"); 386 } 387 388 if (!checkExists.canWrite()) { 389 throw new RuntimeException("The directory " 390 + checkExists.getAbsolutePath() 391 + " is not writable, please set java.io.tempdir" 392 + " to a writable directory"); 393 } 394 395 // create a sub folder with a random number 396 Random ran = new Random(); 397 int x = ran.nextInt(1000000); 398 File f = new File(s, "camel-tmp-" + x); 399 int count = 0; 400 // Let us just try 100 times to avoid the infinite loop 401 while (!f.mkdir()) { 402 count++; 403 if (count >= 100) { 404 throw new RuntimeException("Camel cannot a temp directory from" 405 + checkExists.getAbsolutePath() 406 + " 100 times , please set java.io.tempdir" 407 + " to a writable directory"); 408 } 409 x = ran.nextInt(1000000); 410 f = new File(s, "camel-tmp-" + x); 411 } 412 413 return f; 414 } 415 416 /** 417 * Shutdown and cleanup the temporary directory and removes any shutdown hooks in use. 418 */ 419 @Deprecated 420 public static synchronized void shutdown() { 421 if (defaultTempDir != null && defaultTempDir.exists()) { 422 removeDir(defaultTempDir); 423 } 424 425 if (shutdownHook != null) { 426 Runtime.getRuntime().removeShutdownHook(shutdownHook); 427 shutdownHook = null; 428 } 429 } 430 431 public static void removeDir(File d) { 432 String[] list = d.list(); 433 if (list == null) { 434 list = new String[0]; 435 } 436 for (String s : list) { 437 File f = new File(d, s); 438 if (f.isDirectory()) { 439 removeDir(f); 440 } else { 441 delete(f); 442 } 443 } 444 delete(d); 445 } 446 447 private static void delete(File f) { 448 if (!f.delete()) { 449 if (isWindows()) { 450 System.gc(); 451 } 452 try { 453 Thread.sleep(RETRY_SLEEP_MILLIS); 454 } catch (InterruptedException ex) { 455 // Ignore Exception 456 } 457 if (!f.delete()) { 458 f.deleteOnExit(); 459 } 460 } 461 } 462 463 /** 464 * Renames a file. 465 * 466 * @param from the from file 467 * @param to the to file 468 * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails 469 * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt> 470 * @throws java.io.IOException is thrown if error renaming file 471 */ 472 public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException { 473 // do not try to rename non existing files 474 if (!from.exists()) { 475 return false; 476 } 477 478 // some OS such as Windows can have problem doing rename IO operations so we may need to 479 // retry a couple of times to let it work 480 boolean renamed = false; 481 int count = 0; 482 while (!renamed && count < 3) { 483 if (LOG.isDebugEnabled() && count > 0) { 484 LOG.debug("Retrying attempt {} to rename file from: {} to: {}", count, from, to); 485 } 486 487 renamed = from.renameTo(to); 488 if (!renamed && count > 0) { 489 try { 490 Thread.sleep(1000); 491 } catch (InterruptedException e) { 492 // ignore 493 } 494 } 495 count++; 496 } 497 498 // we could not rename using renameTo, so lets fallback and do a copy/delete approach. 499 // for example if you move files between different file systems (linux -> windows etc.) 500 if (!renamed && copyAndDeleteOnRenameFail) { 501 // now do a copy and delete as all rename attempts failed 502 LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to); 503 renamed = renameFileUsingCopy(from, to); 504 } 505 506 if (LOG.isDebugEnabled() && count > 0) { 507 LOG.debug("Tried {} to rename file: {} to: {} with result: {}", count, from, to, renamed); 508 } 509 return renamed; 510 } 511 512 /** 513 * Rename file using copy and delete strategy. This is primarily used in 514 * environments where the regular rename operation is unreliable. 515 * 516 * @param from the file to be renamed 517 * @param to the new target file 518 * @return <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt> 519 * @throws IOException If an I/O error occurs during copy or delete operations. 520 */ 521 public static boolean renameFileUsingCopy(File from, File to) throws IOException { 522 // do not try to rename non existing files 523 if (!from.exists()) { 524 return false; 525 } 526 527 LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to); 528 529 copyFile(from, to); 530 if (!deleteFile(from)) { 531 throw new IOException("Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from + "' after copy succeeded"); 532 } 533 534 return true; 535 } 536 537 /** 538 * Copies the file 539 * 540 * @param from the source file 541 * @param to the destination file 542 * @throws IOException If an I/O error occurs during copy operation 543 */ 544 public static void copyFile(File from, File to) throws IOException { 545 Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING); 546 } 547 548 /** 549 * Deletes the file. 550 * <p/> 551 * This implementation will attempt to delete the file up till three times with one second delay, which 552 * can mitigate problems on deleting files on some platforms such as Windows. 553 * 554 * @param file the file to delete 555 */ 556 public static boolean deleteFile(File file) { 557 // do not try to delete non existing files 558 if (!file.exists()) { 559 return false; 560 } 561 562 // some OS such as Windows can have problem doing delete IO operations so we may need to 563 // retry a couple of times to let it work 564 boolean deleted = false; 565 int count = 0; 566 while (!deleted && count < 3) { 567 LOG.debug("Retrying attempt {} to delete file: {}", count, file); 568 569 deleted = file.delete(); 570 if (!deleted && count > 0) { 571 try { 572 Thread.sleep(1000); 573 } catch (InterruptedException e) { 574 // ignore 575 } 576 } 577 count++; 578 } 579 580 581 if (LOG.isDebugEnabled() && count > 0) { 582 LOG.debug("Tried {} to delete file: {} with result: {}", count, file, deleted); 583 } 584 return deleted; 585 } 586 587 /** 588 * Is the given file an absolute file. 589 * <p/> 590 * Will also work around issue on Windows to consider files on Windows starting with a \ 591 * as absolute files. This makes the logic consistent across all OS platforms. 592 * 593 * @param file the file 594 * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise. 595 */ 596 public static boolean isAbsolute(File file) { 597 if (isWindows()) { 598 // special for windows 599 String path = file.getPath(); 600 if (path.startsWith(File.separator)) { 601 return true; 602 } 603 } 604 return file.isAbsolute(); 605 } 606 607 /** 608 * Creates a new file. 609 * 610 * @param file the file 611 * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise 612 * @throws IOException is thrown if error creating the new file 613 */ 614 public static boolean createNewFile(File file) throws IOException { 615 // need to check first 616 if (file.exists()) { 617 return false; 618 } 619 try { 620 return file.createNewFile(); 621 } catch (IOException e) { 622 // and check again if the file was created as createNewFile may create the file 623 // but throw a permission error afterwards when using some NAS 624 if (file.exists()) { 625 return true; 626 } else { 627 throw e; 628 } 629 } 630 } 631 632}