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 boolean windowsOs = 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 windowsOs; 084 } 085 086 public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException { 087 Objects.requireNonNull(parentDir); 088 089 if (suffix == null) { 090 suffix = ".tmp"; 091 } 092 if (prefix == null) { 093 prefix = "camel"; 094 } else if (prefix.length() < 3) { 095 prefix = prefix + "camel"; 096 } 097 098 // create parent folder 099 boolean mkdirsResult = parentDir.mkdirs(); 100 if (!mkdirsResult) { 101 LOG.error("mkdirs() failed for " + parentDir); 102 } 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, "" + File.separatorChar); 277 } 278 279 /** 280 * Compacts a path by stacking it and reducing <tt>..</tt>, and uses the given separator. 281 * 282 */ 283 public static String compactPath(String path, char separator) { 284 return compactPath(path, "" + separator); 285 } 286 287 /** 288 * Compacts a file path by stacking it and reducing <tt>..</tt>, and uses the given separator. 289 */ 290 public static String compactPath(String path, String separator) { 291 if (path == null) { 292 return null; 293 } 294 295 if (path.startsWith("http:") || path.startsWith("https:")) { 296 return path; 297 } 298 299 // only normalize if contains a path separator 300 if (path.indexOf('/') == -1 && path.indexOf('\\') == -1) { 301 return path; 302 } 303 304 // need to normalize path before compacting 305 path = normalizePath(path); 306 307 // preserve scheme 308 String scheme = null; 309 if (hasScheme(path)) { 310 int pos = path.indexOf(':'); 311 scheme = path.substring(0, pos); 312 path = path.substring(pos + 1); 313 } 314 315 // preserve ending slash if given in input path 316 boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\"); 317 318 // preserve starting slash if given in input path 319 int cntSlashsAtStart = 0; 320 if (path.startsWith("/") || path.startsWith("\\")) { 321 cntSlashsAtStart++; 322 // for Windows, preserve up to 2 starting slashes, which is necessary for UNC paths. 323 if (isWindows() && path.length() > 1 && (path.charAt(1) == '/' || path.charAt(1) == '\\')) { 324 cntSlashsAtStart++; 325 } 326 } 327 328 Deque<String> stack = new ArrayDeque<>(); 329 330 // separator can either be windows or unix style 331 String separatorRegex = "\\\\|/"; 332 String[] parts = path.split(separatorRegex); 333 for (String part : parts) { 334 if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) { 335 // only pop if there is a previous path, which is not a ".." path either 336 stack.pop(); 337 } else if (part.equals(".") || part.isEmpty()) { 338 // do nothing because we don't want a path like foo/./bar or foo//bar 339 } else { 340 stack.push(part); 341 } 342 } 343 344 // build path based on stack 345 StringBuilder sb = new StringBuilder(); 346 if (scheme != null) { 347 sb.append(scheme); 348 sb.append(":"); 349 } 350 351 for (int i = 0; i < cntSlashsAtStart; i++) { 352 sb.append(separator); 353 } 354 355 // now we build back using FIFO so need to use descending 356 for (Iterator<String> it = stack.descendingIterator(); it.hasNext();) { 357 sb.append(it.next()); 358 if (it.hasNext()) { 359 sb.append(separator); 360 } 361 } 362 363 if (endsWithSlash && !stack.isEmpty()) { 364 sb.append(separator); 365 } 366 367 return sb.toString(); 368 } 369 370 public static void removeDir(File d) { 371 String[] list = d.list(); 372 if (list == null) { 373 list = new String[0]; 374 } 375 for (String s : list) { 376 File f = new File(d, s); 377 if (f.isDirectory()) { 378 removeDir(f); 379 } else { 380 delete(f); 381 } 382 } 383 delete(d); 384 } 385 386 private static void delete(File f) { 387 if (!f.delete()) { 388 // manual GC call on every file delete? Looks very suspicious! 389 /*if (isWindows()) { 390 System.gc(); 391 }*/ 392 try { 393 Thread.sleep(RETRY_SLEEP_MILLIS); 394 } catch (InterruptedException ex) { 395 // Ignore Exception 396 } 397 if (!f.delete()) { 398 f.deleteOnExit(); 399 } 400 } 401 } 402 403 /** 404 * Renames a file. 405 * 406 * @param from the from file 407 * @param to the to file 408 * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails 409 * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt> 410 * @throws java.io.IOException is thrown if error renaming file 411 */ 412 public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException { 413 // do not try to rename non existing files 414 if (!from.exists()) { 415 return false; 416 } 417 418 // some OS such as Windows can have problem doing rename IO operations so we may need to 419 // retry a couple of times to let it work 420 boolean renamed = false; 421 int count = 0; 422 while (!renamed && count < 3) { 423 if (LOG.isDebugEnabled() && count > 0) { 424 LOG.debug("Retrying attempt {} to rename file from: {} to: {}", count, from, to); 425 } 426 427 renamed = from.renameTo(to); 428 if (!renamed && count > 0) { 429 try { 430 Thread.sleep(1000); 431 } catch (InterruptedException e) { 432 // ignore 433 } 434 } 435 count++; 436 } 437 438 // we could not rename using renameTo, so lets fallback and do a copy/delete approach. 439 // for example if you move files between different file systems (linux -> windows etc.) 440 if (!renamed && copyAndDeleteOnRenameFail) { 441 // now do a copy and delete as all rename attempts failed 442 LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to); 443 renamed = renameFileUsingCopy(from, to); 444 } 445 446 if (LOG.isDebugEnabled() && count > 0) { 447 LOG.debug("Tried {} to rename file: {} to: {} with result: {}", count, from, to, renamed); 448 } 449 return renamed; 450 } 451 452 /** 453 * Rename file using copy and delete strategy. This is primarily used in environments where the regular rename 454 * operation is unreliable. 455 * 456 * @param from the file to be renamed 457 * @param to the new target file 458 * @return <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt> 459 * @throws IOException If an I/O error occurs during copy or delete operations. 460 */ 461 public static boolean renameFileUsingCopy(File from, File to) throws IOException { 462 // do not try to rename non existing files 463 if (!from.exists()) { 464 return false; 465 } 466 467 LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to); 468 469 copyFile(from, to); 470 if (!deleteFile(from)) { 471 throw new IOException( 472 "Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from 473 + "' after copy succeeded"); 474 } 475 476 return true; 477 } 478 479 /** 480 * Copies the file 481 * 482 * @param from the source file 483 * @param to the destination file 484 * @throws IOException If an I/O error occurs during copy operation 485 */ 486 public static void copyFile(File from, File to) throws IOException { 487 Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING); 488 } 489 490 /** 491 * Deletes the file. 492 * <p/> 493 * This implementation will attempt to delete the file up till three times with one second delay, which can mitigate 494 * problems on deleting files on some platforms such as Windows. 495 * 496 * @param file the file to delete 497 */ 498 public static boolean deleteFile(File file) { 499 // do not try to delete non existing files 500 if (!file.exists()) { 501 return false; 502 } 503 504 // some OS such as Windows can have problem doing delete IO operations so we may need to 505 // retry a couple of times to let it work 506 boolean deleted = false; 507 int count = 0; 508 while (!deleted && count < 3) { 509 LOG.debug("Retrying attempt {} to delete file: {}", count, file); 510 511 deleted = file.delete(); 512 if (!deleted && count > 0) { 513 try { 514 Thread.sleep(1000); 515 } catch (InterruptedException e) { 516 // ignore 517 } 518 } 519 count++; 520 } 521 522 if (LOG.isDebugEnabled() && count > 0) { 523 LOG.debug("Tried {} to delete file: {} with result: {}", count, file, deleted); 524 } 525 return deleted; 526 } 527 528 /** 529 * Is the given file an absolute file. 530 * <p/> 531 * Will also work around issue on Windows to consider files on Windows starting with a \ as absolute files. This 532 * makes the logic consistent across all OS platforms. 533 * 534 * @param file the file 535 * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise. 536 */ 537 public static boolean isAbsolute(File file) { 538 if (isWindows()) { 539 // special for windows 540 String path = file.getPath(); 541 if (path.startsWith(File.separator)) { 542 return true; 543 } 544 } 545 return file.isAbsolute(); 546 } 547 548 /** 549 * Creates a new file. 550 * 551 * @param file the file 552 * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise 553 * @throws IOException is thrown if error creating the new file 554 */ 555 public static boolean createNewFile(File file) throws IOException { 556 // need to check first 557 if (file.exists()) { 558 return false; 559 } 560 try { 561 return file.createNewFile(); 562 } catch (IOException e) { 563 // and check again if the file was created as createNewFile may create the file 564 // but throw a permission error afterwards when using some NAS 565 if (file.exists()) { 566 return true; 567 } else { 568 throw e; 569 } 570 } 571 } 572 573 /** 574 * Determines whether the URI has a scheme (e.g. file:, classpath: or http:) 575 * 576 * @param uri the URI 577 * @return <tt>true</tt> if the URI starts with a scheme 578 */ 579 private static boolean hasScheme(String uri) { 580 if (uri == null) { 581 return false; 582 } 583 584 return uri.startsWith("file:") || uri.startsWith("classpath:") || uri.startsWith("http:"); 585 } 586 587}