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