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.commons.io; 018 019import java.io.BufferedReader; 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.io.OutputStream; 025import java.nio.charset.Charset; 026import java.time.Duration; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.List; 030import java.util.Locale; 031import java.util.StringTokenizer; 032 033/** 034 * General File System utilities. 035 * <p> 036 * This class provides static utility methods for general file system 037 * functions not provided via the JDK {@link java.io.File File} class. 038 * <p> 039 * The current functions provided are: 040 * <ul> 041 * <li>Get the free space on a drive 042 * </ul> 043 * 044 * @since 1.1 045 * @deprecated As of 2.6 deprecated without replacement. Use equivalent 046 * methods in {@link java.nio.file.FileStore} instead, e.g. 047 * {@code Files.getFileStore(Paths.get("/home")).getUsableSpace()} 048 * or iterate over {@code FileSystems.getDefault().getFileStores()} 049 */ 050@Deprecated 051public class FileSystemUtils { 052 053 /** Singleton instance, used mainly for testing. */ 054 private static final FileSystemUtils INSTANCE = new FileSystemUtils(); 055 056 /** Operating system state flag for error. */ 057 private static final int INIT_PROBLEM = -1; 058 /** Operating system state flag for neither Unix nor Windows. */ 059 private static final int OTHER = 0; 060 /** Operating system state flag for Windows. */ 061 private static final int WINDOWS = 1; 062 /** Operating system state flag for Unix. */ 063 private static final int UNIX = 2; 064 /** Operating system state flag for Posix flavour Unix. */ 065 private static final int POSIX_UNIX = 3; 066 067 /** The operating system flag. */ 068 private static final int OS; 069 070 /** The path to df */ 071 private static final String DF; 072 073 static { 074 int os = OTHER; 075 String dfPath = "df"; 076 try { 077 String osName = System.getProperty("os.name"); 078 if (osName == null) { 079 throw new IOException("os.name not found"); 080 } 081 osName = osName.toLowerCase(Locale.ENGLISH); 082 // match 083 if (osName.contains("windows")) { 084 os = WINDOWS; 085 } else if (osName.contains("linux") || 086 osName.contains("mpe/ix") || 087 osName.contains("freebsd") || 088 osName.contains("openbsd") || 089 osName.contains("irix") || 090 osName.contains("digital unix") || 091 osName.contains("unix") || 092 osName.contains("mac os x")) { 093 os = UNIX; 094 } else if (osName.contains("sun os") || 095 osName.contains("sunos") || 096 osName.contains("solaris")) { 097 os = POSIX_UNIX; 098 dfPath = "/usr/xpg4/bin/df"; 099 } else if (osName.contains("hp-ux") || 100 osName.contains("aix")) { 101 os = POSIX_UNIX; 102 } 103 104 } catch (final Exception ex) { 105 os = INIT_PROBLEM; 106 } 107 OS = os; 108 DF = dfPath; 109 } 110 111 /** 112 * Instances should NOT be constructed in standard programming. 113 */ 114 public FileSystemUtils() { 115 } 116 117 /** 118 * Returns the free space on a drive or volume by invoking 119 * the command line. 120 * This method does not normalize the result, and typically returns 121 * bytes on Windows, 512 byte units on OS X and kilobytes on Unix. 122 * As this is not very useful, this method is deprecated in favour 123 * of {@link #freeSpaceKb(String)} which returns a result in kilobytes. 124 * <p> 125 * Note that some OS's are NOT currently supported, including OS/390, 126 * OpenVMS. 127 * <pre> 128 * FileSystemUtils.freeSpace("C:"); // Windows 129 * FileSystemUtils.freeSpace("/volume"); // *nix 130 * </pre> 131 * The free space is calculated via the command line. 132 * It uses 'dir /-c' on Windows and 'df' on *nix. 133 * 134 * @param path the path to get free space for, not null, not empty on Unix 135 * @return the amount of free drive space on the drive or volume 136 * @throws IllegalArgumentException if the path is invalid 137 * @throws IllegalStateException if an error occurred in initialisation 138 * @throws IOException if an error occurs when finding the free space 139 * @since 1.1, enhanced OS support in 1.2 and 1.3 140 * @deprecated Use freeSpaceKb(String) 141 * Deprecated from 1.3, may be removed in 2.0 142 */ 143 @Deprecated 144 public static long freeSpace(final String path) throws IOException { 145 return INSTANCE.freeSpaceOS(path, OS, false, Duration.ofMillis(-1)); 146 } 147 148 /** 149 * Returns the free space on a drive or volume in kibibytes (1024 bytes) 150 * by invoking the command line. 151 * <pre> 152 * FileSystemUtils.freeSpaceKb("C:"); // Windows 153 * FileSystemUtils.freeSpaceKb("/volume"); // *nix 154 * </pre> 155 * The free space is calculated via the command line. 156 * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. 157 * <p> 158 * In order to work, you must be running Windows, or have a implementation of 159 * Unix df that supports GNU format when passed -k (or -kP). If you are going 160 * to rely on this code, please check that it works on your OS by running 161 * some simple tests to compare the command line with the output from this class. 162 * If your operating system isn't supported, please raise a JIRA call detailing 163 * the exact result from df -k and as much other detail as possible, thanks. 164 * 165 * @param path the path to get free space for, not null, not empty on Unix 166 * @return the amount of free drive space on the drive or volume in kilobytes 167 * @throws IllegalArgumentException if the path is invalid 168 * @throws IllegalStateException if an error occurred in initialisation 169 * @throws IOException if an error occurs when finding the free space 170 * @since 1.2, enhanced OS support in 1.3 171 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 172 */ 173 @Deprecated 174 public static long freeSpaceKb(final String path) throws IOException { 175 return freeSpaceKb(path, -1); 176 } 177 /** 178 * Returns the free space on a drive or volume in kibibytes (1024 bytes) 179 * by invoking the command line. 180 * <pre> 181 * FileSystemUtils.freeSpaceKb("C:"); // Windows 182 * FileSystemUtils.freeSpaceKb("/volume"); // *nix 183 * </pre> 184 * The free space is calculated via the command line. 185 * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. 186 * <p> 187 * In order to work, you must be running Windows, or have a implementation of 188 * Unix df that supports GNU format when passed -k (or -kP). If you are going 189 * to rely on this code, please check that it works on your OS by running 190 * some simple tests to compare the command line with the output from this class. 191 * If your operating system isn't supported, please raise a JIRA call detailing 192 * the exact result from df -k and as much other detail as possible, thanks. 193 * 194 * @param path the path to get free space for, not null, not empty on Unix 195 * @param timeout The timeout amount in milliseconds or no timeout if the value 196 * is zero or less 197 * @return the amount of free drive space on the drive or volume in kilobytes 198 * @throws IllegalArgumentException if the path is invalid 199 * @throws IllegalStateException if an error occurred in initialisation 200 * @throws IOException if an error occurs when finding the free space 201 * @since 2.0 202 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 203 */ 204 @Deprecated 205 public static long freeSpaceKb(final String path, final long timeout) throws IOException { 206 return INSTANCE.freeSpaceOS(path, OS, true, Duration.ofMillis(timeout)); 207 } 208 209 /** 210 * Returns the free space for the working directory 211 * in kibibytes (1024 bytes) by invoking the command line. 212 * <p> 213 * Identical to: 214 * <pre> 215 * freeSpaceKb(new File(".").getAbsolutePath()) 216 * </pre> 217 * @return the amount of free drive space on the drive or volume in kilobytes 218 * @throws IllegalStateException if an error occurred in initialisation 219 * @throws IOException if an error occurs when finding the free space 220 * @since 2.0 221 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 222 */ 223 @Deprecated 224 public static long freeSpaceKb() throws IOException { 225 return freeSpaceKb(-1); 226 } 227 228 /** 229 * Returns the free space for the working directory 230 * in kibibytes (1024 bytes) by invoking the command line. 231 * <p> 232 * Identical to: 233 * <pre> 234 * freeSpaceKb(new File(".").getAbsolutePath()) 235 * </pre> 236 * @param timeout The timeout amount in milliseconds or no timeout if the value 237 * is zero or less 238 * @return the amount of free drive space on the drive or volume in kilobytes 239 * @throws IllegalStateException if an error occurred in initialisation 240 * @throws IOException if an error occurs when finding the free space 241 * @since 2.0 242 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 243 */ 244 @Deprecated 245 public static long freeSpaceKb(final long timeout) throws IOException { 246 return freeSpaceKb(new File(".").getAbsolutePath(), timeout); 247 } 248 249 /** 250 * Returns the free space on a drive or volume in a cross-platform manner. 251 * Note that some OS's are NOT currently supported, including OS/390. 252 * <pre> 253 * FileSystemUtils.freeSpace("C:"); // Windows 254 * FileSystemUtils.freeSpace("/volume"); // *nix 255 * </pre> 256 * The free space is calculated via the command line. 257 * It uses 'dir /-c' on Windows and 'df' on *nix. 258 * 259 * @param path the path to get free space for, not null, not empty on Unix 260 * @param os the operating system code 261 * @param kb whether to normalize to kilobytes 262 * @param timeout The timeout amount in milliseconds or no timeout if the value 263 * is zero or less 264 * @return the amount of free drive space on the drive or volume 265 * @throws IllegalArgumentException if the path is invalid 266 * @throws IllegalStateException if an error occurred in initialisation 267 * @throws IOException if an error occurs when finding the free space 268 */ 269 long freeSpaceOS(final String path, final int os, final boolean kb, final Duration timeout) throws IOException { 270 if (path == null) { 271 throw new IllegalArgumentException("Path must not be null"); 272 } 273 switch (os) { 274 case WINDOWS: 275 return kb ? freeSpaceWindows(path, timeout) / FileUtils.ONE_KB : freeSpaceWindows(path, timeout); 276 case UNIX: 277 return freeSpaceUnix(path, kb, false, timeout); 278 case POSIX_UNIX: 279 return freeSpaceUnix(path, kb, true, timeout); 280 case OTHER: 281 throw new IllegalStateException("Unsupported operating system"); 282 default: 283 throw new IllegalStateException( 284 "Exception caught when determining operating system"); 285 } 286 } 287 288 /** 289 * Find free space on the Windows platform using the 'dir' command. 290 * 291 * @param path the path to get free space for, including the colon 292 * @param timeout The timeout amount in milliseconds or no timeout if the value 293 * is zero or less 294 * @return the amount of free drive space on the drive 295 * @throws IOException if an error occurs 296 */ 297 long freeSpaceWindows(final String path, final Duration timeout) throws IOException { 298 String normPath = FilenameUtils.normalize(path, false); 299 if (normPath == null) { 300 throw new IllegalArgumentException(path); 301 } 302 if (!normPath.isEmpty() && normPath.charAt(0) != '"') { 303 normPath = "\"" + normPath + "\""; 304 } 305 306 // build and run the 'dir' command 307 final String[] cmdAttribs = {"cmd.exe", "/C", "dir /a /-c " + normPath}; 308 309 // read in the output of the command to an ArrayList 310 final List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE, timeout); 311 312 // now iterate over the lines we just read and find the LAST 313 // non-empty line (the free space bytes should be in the last element 314 // of the ArrayList anyway, but this will ensure it works even if it's 315 // not, still assuming it is on the last non-blank line) 316 for (int i = lines.size() - 1; i >= 0; i--) { 317 final String line = lines.get(i); 318 if (!line.isEmpty()) { 319 return parseDir(line, normPath); 320 } 321 } 322 // all lines are blank 323 throw new IOException( 324 "Command line 'dir /-c' did not return any info " + 325 "for path '" + normPath + "'"); 326 } 327 328 /** 329 * Parses the Windows dir response last line 330 * 331 * @param line the line to parse 332 * @param path the path that was sent 333 * @return the number of bytes 334 * @throws IOException if an error occurs 335 */ 336 long parseDir(final String line, final String path) throws IOException { 337 // read from the end of the line to find the last numeric 338 // character on the line, then continue until we find the first 339 // non-numeric character, and everything between that and the last 340 // numeric character inclusive is our free space bytes count 341 int bytesStart = 0; 342 int bytesEnd = 0; 343 int j = line.length() - 1; 344 innerLoop1: while (j >= 0) { 345 final char c = line.charAt(j); 346 if (Character.isDigit(c)) { 347 // found the last numeric character, this is the end of 348 // the free space bytes count 349 bytesEnd = j + 1; 350 break innerLoop1; 351 } 352 j--; 353 } 354 innerLoop2: while (j >= 0) { 355 final char c = line.charAt(j); 356 if (!Character.isDigit(c) && c != ',' && c != '.') { 357 // found the next non-numeric character, this is the 358 // beginning of the free space bytes count 359 bytesStart = j + 1; 360 break innerLoop2; 361 } 362 j--; 363 } 364 if (j < 0) { 365 throw new IOException( 366 "Command line 'dir /-c' did not return valid info " + 367 "for path '" + path + "'"); 368 } 369 370 // remove commas and dots in the bytes count 371 final StringBuilder buf = new StringBuilder(line.substring(bytesStart, bytesEnd)); 372 for (int k = 0; k < buf.length(); k++) { 373 if (buf.charAt(k) == ',' || buf.charAt(k) == '.') { 374 buf.deleteCharAt(k--); 375 } 376 } 377 return parseBytes(buf.toString(), path); 378 } 379 380 /** 381 * Find free space on the *nix platform using the 'df' command. 382 * 383 * @param path the path to get free space for 384 * @param kb whether to normalize to kilobytes 385 * @param posix whether to use the POSIX standard format flag 386 * @param timeout The timeout amount in milliseconds or no timeout if the value 387 * is zero or less 388 * @return the amount of free drive space on the volume 389 * @throws IOException if an error occurs 390 */ 391 long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout) 392 throws IOException { 393 if (path.isEmpty()) { 394 throw new IllegalArgumentException("Path must not be empty"); 395 } 396 397 // build and run the 'dir' command 398 String flags = "-"; 399 if (kb) { 400 flags += "k"; 401 } 402 if (posix) { 403 flags += "P"; 404 } 405 final String[] cmdAttribs = 406 flags.length() > 1 ? new String[] {DF, flags, path} : new String[] {DF, path}; 407 408 // perform the command, asking for up to 3 lines (header, interesting, overflow) 409 final List<String> lines = performCommand(cmdAttribs, 3, timeout); 410 if (lines.size() < 2) { 411 // unknown problem, throw exception 412 throw new IOException( 413 "Command line '" + DF + "' did not return info as expected " + 414 "for path '" + path + "'- response was " + lines); 415 } 416 final String line2 = lines.get(1); // the line we're interested in 417 418 // Now, we tokenize the string. The fourth element is what we want. 419 StringTokenizer tok = new StringTokenizer(line2, " "); 420 if (tok.countTokens() < 4) { 421 // could be long Filesystem, thus data on third line 422 if ((tok.countTokens() != 1) || (lines.size() < 3)) { 423 throw new IOException( 424 "Command line '" + DF + "' did not return data as expected " + 425 "for path '" + path + "'- check path is valid"); 426 } 427 final String line3 = lines.get(2); // the line may be interested in 428 tok = new StringTokenizer(line3, " "); 429 } else { 430 tok.nextToken(); // Ignore Filesystem 431 } 432 tok.nextToken(); // Ignore 1K-blocks 433 tok.nextToken(); // Ignore Used 434 final String freeSpace = tok.nextToken(); 435 return parseBytes(freeSpace, path); 436 } 437 438 /** 439 * Parses the bytes from a string. 440 * 441 * @param freeSpace the free space string 442 * @param path the path 443 * @return the number of bytes 444 * @throws IOException if an error occurs 445 */ 446 long parseBytes(final String freeSpace, final String path) throws IOException { 447 try { 448 final long bytes = Long.parseLong(freeSpace); 449 if (bytes < 0) { 450 throw new IOException( 451 "Command line '" + DF + "' did not find free space in response " + 452 "for path '" + path + "'- check path is valid"); 453 } 454 return bytes; 455 456 } catch (final NumberFormatException ex) { 457 throw new IOException( 458 "Command line '" + DF + "' did not return numeric data as expected " + 459 "for path '" + path + "'- check path is valid", ex); 460 } 461 } 462 463 /** 464 * Performs an OS command. 465 * 466 * @param cmdAttribs the command line parameters 467 * @param max The maximum limit for the lines returned 468 * @param timeout The timeout amount in milliseconds or no timeout if the value 469 * is zero or less 470 * @return the lines returned by the command, converted to lower-case 471 * @throws IOException if an error occurs 472 */ 473 List<String> performCommand(final String[] cmdAttribs, final int max, final Duration timeout) throws IOException { 474 // this method does what it can to avoid the 'Too many open files' error 475 // based on trial and error and these links: 476 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692 477 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027 478 // http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018 479 // however, its still not perfect as the JDK support is so poor 480 // (see commons-exec or Ant for a better multi-threaded multi-os solution) 481 482 final List<String> lines = new ArrayList<>(20); 483 Process proc = null; 484 InputStream in = null; 485 OutputStream out = null; 486 InputStream err = null; 487 BufferedReader inr = null; 488 try { 489 490 final Thread monitor = ThreadMonitor.start(timeout); 491 492 proc = openProcess(cmdAttribs); 493 in = proc.getInputStream(); 494 out = proc.getOutputStream(); 495 err = proc.getErrorStream(); 496 // default charset is most likely appropriate here 497 inr = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset())); 498 String line = inr.readLine(); 499 while (line != null && lines.size() < max) { 500 line = line.toLowerCase(Locale.ENGLISH).trim(); 501 lines.add(line); 502 line = inr.readLine(); 503 } 504 505 proc.waitFor(); 506 507 ThreadMonitor.stop(monitor); 508 509 if (proc.exitValue() != 0) { 510 // os command problem, throw exception 511 throw new IOException( 512 "Command line returned OS error code '" + proc.exitValue() + 513 "' for command " + Arrays.asList(cmdAttribs)); 514 } 515 if (lines.isEmpty()) { 516 // unknown problem, throw exception 517 throw new IOException( 518 "Command line did not return any info " + 519 "for command " + Arrays.asList(cmdAttribs)); 520 } 521 522 inr.close(); 523 inr = null; 524 525 in.close(); 526 in = null; 527 528 if (out != null) { 529 out.close(); 530 out = null; 531 } 532 533 if (err != null) { 534 err.close(); 535 err = null; 536 } 537 538 return lines; 539 540 } catch (final InterruptedException ex) { 541 throw new IOException( 542 "Command line threw an InterruptedException " + 543 "for command " + Arrays.asList(cmdAttribs) + " timeout=" + timeout, ex); 544 } finally { 545 IOUtils.closeQuietly(in); 546 IOUtils.closeQuietly(out); 547 IOUtils.closeQuietly(err); 548 IOUtils.closeQuietly(inr); 549 if (proc != null) { 550 proc.destroy(); 551 } 552 } 553 } 554 555 /** 556 * Opens the process to the operating system. 557 * 558 * @param cmdAttribs the command line parameters 559 * @return the process 560 * @throws IOException if an error occurs 561 */ 562 Process openProcess(final String[] cmdAttribs) throws IOException { 563 return Runtime.getRuntime().exec(cmdAttribs); 564 } 565 566}