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