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