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