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