1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.io; 18 19 import java.io.BufferedReader; 20 import java.io.IOException; 21 import java.io.InputStream; 22 import java.io.InputStreamReader; 23 import java.io.OutputStream; 24 import java.nio.charset.Charset; 25 import java.time.Duration; 26 import java.util.Arrays; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.Objects; 30 import java.util.StringTokenizer; 31 import java.util.stream.Collectors; 32 33 /** 34 * General File System utilities. 35 * <p> 36 * This class provides static utility methods for general file system 37 * functions not provided via the JDK {@link java.io.File File} class. 38 * <p> 39 * The current functions provided are: 40 * <ul> 41 * <li>Get the free space on a drive 42 * </ul> 43 * 44 * @since 1.1 45 * @deprecated As of 2.6 deprecated without replacement. Use equivalent 46 * methods in {@link java.nio.file.FileStore} instead, e.g. 47 * {@code Files.getFileStore(Paths.get("/home")).getUsableSpace()} 48 * or iterate over {@code FileSystems.getDefault().getFileStores()} 49 */ 50 @Deprecated 51 public class FileSystemUtils { 52 53 /** Singleton instance, used mainly for testing. */ 54 private static final FileSystemUtils INSTANCE = new FileSystemUtils(); 55 56 /** Operating system state flag for error. */ 57 private static final int INIT_PROBLEM = -1; 58 /** Operating system state flag for neither Unix nor Windows. */ 59 private static final int OTHER = 0; 60 /** Operating system state flag for Windows. */ 61 private static final int WINDOWS = 1; 62 /** Operating system state flag for Unix. */ 63 private static final int UNIX = 2; 64 /** Operating system state flag for Posix flavour Unix. */ 65 private static final int POSIX_UNIX = 3; 66 67 /** The operating system flag. */ 68 private static final int OS; 69 70 /** The path to df */ 71 private static final String DF; 72 73 static { 74 int os = OTHER; 75 String dfPath = "df"; 76 try { 77 String osName = System.getProperty("os.name"); 78 if (osName == null) { 79 throw new IOException("os.name not found"); 80 } 81 osName = osName.toLowerCase(Locale.ENGLISH); 82 // match 83 if (osName.contains("windows")) { 84 os = WINDOWS; 85 } else if (osName.contains("linux") || 86 osName.contains("mpe/ix") || 87 osName.contains("freebsd") || 88 osName.contains("openbsd") || 89 osName.contains("irix") || 90 osName.contains("digital unix") || 91 osName.contains("unix") || 92 osName.contains("mac os x")) { 93 os = UNIX; 94 } else if (osName.contains("sun os") || 95 osName.contains("sunos") || 96 osName.contains("solaris")) { 97 os = POSIX_UNIX; 98 dfPath = "/usr/xpg4/bin/df"; 99 } 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 * Returns the free space on a drive or volume by invoking 113 * the command line. 114 * This method does not normalize the result, and typically returns 115 * bytes on Windows, 512 byte units on OS X and kilobytes on Unix. 116 * As this is not very useful, this method is deprecated in favor 117 * of {@link #freeSpaceKb(String)} which returns a result in kilobytes. 118 * <p> 119 * Note that some OS's are NOT currently supported, including OS/390, 120 * OpenVMS. 121 * <pre> 122 * FileSystemUtils.freeSpace("C:"); // Windows 123 * FileSystemUtils.freeSpace("/volume"); // *nix 124 * </pre> 125 * The free space is calculated via the command line. 126 * It uses 'dir /-c' on Windows and 'df' on *nix. 127 * 128 * @param path the path to get free space for, not null, not empty on Unix 129 * @return the amount of free drive space on the drive or volume 130 * @throws IllegalArgumentException if the path is invalid 131 * @throws IllegalStateException if an error occurred in initialization 132 * @throws IOException if an error occurs when finding the free space 133 * @since 1.1, enhanced OS support in 1.2 and 1.3 134 * @deprecated Use freeSpaceKb(String) 135 * Deprecated from 1.3, may be removed in 2.0 136 */ 137 @Deprecated freeSpace(final String path)138 public static long freeSpace(final String path) throws IOException { 139 return INSTANCE.freeSpaceOS(path, OS, false, Duration.ofMillis(-1)); 140 } 141 142 /** 143 * Returns the free space for the working directory 144 * in kibibytes (1024 bytes) by invoking the command line. 145 * <p> 146 * Identical to: 147 * <pre> 148 * freeSpaceKb(FileUtils.current().getAbsolutePath()) 149 * </pre> 150 * @return the amount of free drive space on the drive or volume in kilobytes 151 * @throws IllegalStateException if an error occurred in initialization 152 * @throws IOException if an error occurs when finding the free space 153 * @since 2.0 154 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 155 */ 156 @Deprecated freeSpaceKb()157 public static long freeSpaceKb() throws IOException { 158 return freeSpaceKb(-1); 159 } 160 161 /** 162 * Returns the free space for the working directory 163 * in kibibytes (1024 bytes) by invoking the command line. 164 * <p> 165 * Identical to: 166 * <pre> 167 * freeSpaceKb(FileUtils.current().getAbsolutePath()) 168 * </pre> 169 * @param timeout The timeout amount in milliseconds or no timeout if the value 170 * is zero or less 171 * @return the amount of free drive space on the drive or volume in kilobytes 172 * @throws IllegalStateException if an error occurred in initialization 173 * @throws IOException if an error occurs when finding the free space 174 * @since 2.0 175 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 176 */ 177 @Deprecated freeSpaceKb(final long timeout)178 public static long freeSpaceKb(final long timeout) throws IOException { 179 return freeSpaceKb(FileUtils.current().getAbsolutePath(), timeout); 180 } 181 /** 182 * Returns the free space on a drive or volume in kibibytes (1024 bytes) 183 * by invoking the command line. 184 * <pre> 185 * FileSystemUtils.freeSpaceKb("C:"); // Windows 186 * FileSystemUtils.freeSpaceKb("/volume"); // *nix 187 * </pre> 188 * The free space is calculated via the command line. 189 * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. 190 * <p> 191 * In order to work, you must be running Windows, or have an implementation of 192 * Unix df that supports GNU format when passed -k (or -kP). If you are going 193 * to rely on this code, please check that it works on your OS by running 194 * some simple tests to compare the command line with the output from this class. 195 * If your operating system isn't supported, please raise a JIRA call detailing 196 * the exact result from df -k and as much other detail as possible, thanks. 197 * 198 * @param path the path to get free space for, not null, not empty on Unix 199 * @return the amount of free drive space on the drive or volume in kilobytes 200 * @throws IllegalArgumentException if the path is invalid 201 * @throws IllegalStateException if an error occurred in initialization 202 * @throws IOException if an error occurs when finding the free space 203 * @since 1.2, enhanced OS support in 1.3 204 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 205 */ 206 @Deprecated freeSpaceKb(final String path)207 public static long freeSpaceKb(final String path) throws IOException { 208 return freeSpaceKb(path, -1); 209 } 210 211 /** 212 * Returns the free space on a drive or volume in kibibytes (1024 bytes) 213 * by invoking the command line. 214 * <pre> 215 * FileSystemUtils.freeSpaceKb("C:"); // Windows 216 * FileSystemUtils.freeSpaceKb("/volume"); // *nix 217 * </pre> 218 * The free space is calculated via the command line. 219 * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. 220 * <p> 221 * In order to work, you must be running Windows, or have an implementation of 222 * Unix df that supports GNU format when passed -k (or -kP). If you are going 223 * to rely on this code, please check that it works on your OS by running 224 * some simple tests to compare the command line with the output from this class. 225 * If your operating system isn't supported, please raise a JIRA call detailing 226 * the exact result from df -k and as much other detail as possible, thanks. 227 * 228 * @param path the path to get free space for, not null, not empty on Unix 229 * @param timeout The timeout amount in milliseconds or no timeout if the value 230 * is zero or less 231 * @return the amount of free drive space on the drive or volume in kilobytes 232 * @throws IllegalArgumentException if the path is invalid 233 * @throws IllegalStateException if an error occurred in initialization 234 * @throws IOException if an error occurs when finding the free space 235 * @since 2.0 236 * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}. 237 */ 238 @Deprecated freeSpaceKb(final String path, final long timeout)239 public static long freeSpaceKb(final String path, final long timeout) throws IOException { 240 return INSTANCE.freeSpaceOS(path, OS, true, Duration.ofMillis(timeout)); 241 } 242 243 /** 244 * Instances should NOT be constructed in standard programming. 245 */ FileSystemUtils()246 public FileSystemUtils() { 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 initialization 267 * @throws IOException if an error occurs when finding the free space 268 */ freeSpaceOS(final String path, final int os, final boolean kb, final Duration timeout)269 long freeSpaceOS(final String path, final int os, final boolean kb, final Duration timeout) throws IOException { 270 Objects.requireNonNull(path, "path"); 271 switch (os) { 272 case WINDOWS: 273 return kb ? freeSpaceWindows(path, timeout) / FileUtils.ONE_KB : freeSpaceWindows(path, timeout); 274 case UNIX: 275 return freeSpaceUnix(path, kb, false, timeout); 276 case POSIX_UNIX: 277 return freeSpaceUnix(path, kb, true, timeout); 278 case OTHER: 279 throw new IllegalStateException("Unsupported operating system"); 280 default: 281 throw new IllegalStateException("Exception caught when determining operating system"); 282 } 283 } 284 285 /** 286 * Find free space on the *nix platform using the 'df' command. 287 * 288 * @param path the path to get free space for 289 * @param kb whether to normalize to kilobytes 290 * @param posix whether to use the POSIX standard format flag 291 * @param timeout The timeout amount in milliseconds or no timeout if the value 292 * is zero or less 293 * @return the amount of free drive space on the volume 294 * @throws IOException If an I/O error occurs 295 */ freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout)296 long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout) 297 throws IOException { 298 if (path.isEmpty()) { 299 throw new IllegalArgumentException("Path must not be empty"); 300 } 301 302 // build and run the 'dir' command 303 String flags = "-"; 304 if (kb) { 305 flags += "k"; 306 } 307 if (posix) { 308 flags += "P"; 309 } 310 final String[] cmdAttribs = flags.length() > 1 ? new String[] { DF, flags, path } : new String[] { DF, path }; 311 312 // perform the command, asking for up to 3 lines (header, interesting, overflow) 313 final List<String> lines = performCommand(cmdAttribs, 3, timeout); 314 if (lines.size() < 2) { 315 // unknown problem, throw exception 316 throw new IOException("Command line '" + DF + "' did not return info as expected for path '" + path + "'- response was " + lines); 317 } 318 final String line2 = lines.get(1); // the line we're interested in 319 320 // Now, we tokenize the string. The fourth element is what we want. 321 StringTokenizer tok = new StringTokenizer(line2, " "); 322 if (tok.countTokens() < 4) { 323 // could be long Filesystem, thus data on third line 324 if (tok.countTokens() != 1 || lines.size() < 3) { 325 throw new IOException("Command line '" + DF + "' did not return data as expected for path '" + path + "'- check path is valid"); 326 } 327 final String line3 = lines.get(2); // the line may be interested in 328 tok = new StringTokenizer(line3, " "); 329 } else { 330 tok.nextToken(); // Ignore Filesystem 331 } 332 tok.nextToken(); // Ignore 1K-blocks 333 tok.nextToken(); // Ignore Used 334 final String freeSpace = tok.nextToken(); 335 return parseBytes(freeSpace, path); 336 } 337 338 /** 339 * Find free space on the Windows platform using the 'dir' command. 340 * 341 * @param path the path to get free space for, including the colon 342 * @param timeout The timeout amount in milliseconds or no timeout if the value 343 * is zero or less 344 * @return the amount of free drive space on the drive 345 * @throws IOException If an I/O error occurs 346 */ freeSpaceWindows(final String path, final Duration timeout)347 long freeSpaceWindows(final String path, final Duration timeout) throws IOException { 348 String normPath = FilenameUtils.normalize(path, false); 349 if (normPath == null) { 350 throw new IllegalArgumentException(path); 351 } 352 if (!normPath.isEmpty() && normPath.charAt(0) != '"') { 353 normPath = "\"" + normPath + "\""; 354 } 355 356 // build and run the 'dir' command 357 final String[] cmdAttribs = { "cmd.exe", "/C", "dir /a /-c " + normPath }; 358 359 // read in the output of the command to an ArrayList 360 final List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE, timeout); 361 362 // now iterate over the lines we just read and find the LAST 363 // non-empty line (the free space bytes should be in the last element 364 // of the ArrayList anyway, but this will ensure it works even if it's 365 // not, still assuming it is on the last non-blank line) 366 for (int i = lines.size() - 1; i >= 0; i--) { 367 final String line = lines.get(i); 368 if (!line.isEmpty()) { 369 return parseDir(line, normPath); 370 } 371 } 372 // all lines are blank 373 throw new IOException("Command line 'dir /-c' did not return any info for path '" + normPath + "'"); 374 } 375 376 /** 377 * Opens the process to the operating system. 378 * 379 * @param cmdAttribs the command line parameters 380 * @return the process 381 * @throws IOException If an I/O error occurs 382 */ openProcess(final String[] cmdAttribs)383 Process openProcess(final String[] cmdAttribs) throws IOException { 384 return Runtime.getRuntime().exec(cmdAttribs); 385 } 386 387 /** 388 * Parses the bytes from a string. 389 * 390 * @param freeSpace the free space string 391 * @param path the path 392 * @return the number of bytes 393 * @throws IOException If an I/O error occurs 394 */ parseBytes(final String freeSpace, final String path)395 long parseBytes(final String freeSpace, final String path) throws IOException { 396 try { 397 final long bytes = Long.parseLong(freeSpace); 398 if (bytes < 0) { 399 throw new IOException("Command line '" + DF + "' did not find free space in response for path '" + path + "'- check path is valid"); 400 } 401 return bytes; 402 403 } catch (final NumberFormatException ex) { 404 throw new IOException("Command line '" + DF + "' did not return numeric data as expected for path '" + path + "'- check path is valid", ex); 405 } 406 } 407 408 /** 409 * Parses the Windows dir response last line 410 * 411 * @param line the line to parse 412 * @param path the path that was sent 413 * @return the number of bytes 414 * @throws IOException If an I/O error occurs 415 */ parseDir(final String line, final String path)416 long parseDir(final String line, final String path) throws IOException { 417 // read from the end of the line to find the last numeric 418 // character on the line, then continue until we find the first 419 // non-numeric character, and everything between that and the last 420 // numeric character inclusive is our free space bytes count 421 int bytesStart = 0; 422 int bytesEnd = 0; 423 int j = line.length() - 1; 424 innerLoop1: while (j >= 0) { 425 final char c = line.charAt(j); 426 if (Character.isDigit(c)) { 427 // found the last numeric character, this is the end of 428 // the free space bytes count 429 bytesEnd = j + 1; 430 break innerLoop1; 431 } 432 j--; 433 } 434 innerLoop2: while (j >= 0) { 435 final char c = line.charAt(j); 436 if (!Character.isDigit(c) && c != ',' && c != '.') { 437 // found the next non-numeric character, this is the 438 // beginning of the free space bytes count 439 bytesStart = j + 1; 440 break innerLoop2; 441 } 442 j--; 443 } 444 if (j < 0) { 445 throw new IOException("Command line 'dir /-c' did not return valid info for path '" + path + "'"); 446 } 447 448 // remove commas and dots in the bytes count 449 final StringBuilder buf = new StringBuilder(line.substring(bytesStart, bytesEnd)); 450 for (int k = 0; k < buf.length(); k++) { 451 if (buf.charAt(k) == ',' || buf.charAt(k) == '.') { 452 buf.deleteCharAt(k--); 453 } 454 } 455 return parseBytes(buf.toString(), path); 456 } 457 458 /** 459 * Performs an OS command. 460 * 461 * @param cmdAttribs the command line parameters 462 * @param max The maximum limit for the lines returned 463 * @param timeout The timeout amount in milliseconds or no timeout if the value 464 * is zero or less 465 * @return the lines returned by the command, converted to lower-case 466 * @throws IOException if an error occurs 467 */ performCommand(final String[] cmdAttribs, final int max, final Duration timeout)468 List<String> performCommand(final String[] cmdAttribs, final int max, final Duration timeout) throws IOException { 469 // 470 // This method does what it can to avoid the 'Too many open files' error 471 // based on trial and error and these links: 472 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692 473 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027 474 // http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018 475 // however, it's still not perfect as the JDK support is so poor 476 // (see commons-exec or Ant for a better multithreaded multi-OS solution) 477 // 478 final Process proc = openProcess(cmdAttribs); 479 final Thread monitor = ThreadMonitor.start(timeout); 480 try (InputStream in = proc.getInputStream(); 481 OutputStream out = proc.getOutputStream(); 482 // default Charset is most likely appropriate here 483 InputStream err = proc.getErrorStream(); 484 // If in is null here, InputStreamReader throws NullPointerException 485 BufferedReader inr = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { 486 487 final List<String> lines = inr.lines().limit(max).map(line -> line.toLowerCase(Locale.getDefault()).trim()).collect(Collectors.toList()); 488 proc.waitFor(); 489 ThreadMonitor.stop(monitor); 490 491 if (proc.exitValue() != 0) { 492 // Command problem, throw exception 493 throw new IOException("Command line returned OS error code '" + proc.exitValue() + "' for command " + Arrays.asList(cmdAttribs)); 494 } 495 if (lines.isEmpty()) { 496 // Unknown problem, throw exception 497 throw new IOException("Command line did not return any info for command " + Arrays.asList(cmdAttribs)); 498 } 499 500 return lines; 501 502 } catch (final InterruptedException ex) { 503 throw new IOException("Command line threw an InterruptedException for command " + Arrays.asList(cmdAttribs) + " timeout=" + timeout, ex); 504 } finally { 505 if (proc != null) { 506 proc.destroy(); 507 } 508 } 509 } 510 511 } 512