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