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