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