FileSystemUtils.java revision c28bf353190eb576072a8fd2f98821424144876e
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 */ 17package org.apache.commons.io; 18 19import java.io.BufferedReader; 20import java.io.IOException; 21import java.io.InputStream; 22import java.io.InputStreamReader; 23import java.io.OutputStream; 24import java.util.ArrayList; 25import java.util.Arrays; 26import java.util.List; 27import java.util.StringTokenizer; 28 29/** 30 * General File System utilities. 31 * <p> 32 * This class provides static utility methods for general file system 33 * functions not provided via the JDK {@link java.io.File File} class. 34 * <p> 35 * The current functions provided are: 36 * <ul> 37 * <li>Get the free space on a drive 38 * </ul> 39 * 40 * @author Frank W. Zammetti 41 * @author Stephen Colebourne 42 * @author Thomas Ledoux 43 * @author James Urie 44 * @author Magnus Grimsell 45 * @author Thomas Ledoux 46 * @version $Id: FileSystemUtils.java 453889 2006-10-07 11:56:25Z scolebourne $ 47 * @since Commons IO 1.1 48 */ 49public class FileSystemUtils { 50 51 /** Singleton instance, used mainly for testing. */ 52 private static final FileSystemUtils INSTANCE = new FileSystemUtils(); 53 54 /** Operating system state flag for error. */ 55 private static final int INIT_PROBLEM = -1; 56 /** Operating system state flag for neither Unix nor Windows. */ 57 private static final int OTHER = 0; 58 /** Operating system state flag for Windows. */ 59 private static final int WINDOWS = 1; 60 /** Operating system state flag for Unix. */ 61 private static final int UNIX = 2; 62 /** Operating system state flag for Posix flavour Unix. */ 63 private static final int POSIX_UNIX = 3; 64 65 /** The operating system flag. */ 66 private static final int OS; 67 static { 68 int os = OTHER; 69 try { 70 String osName = System.getProperty("os.name"); 71 if (osName == null) { 72 throw new IOException("os.name not found"); 73 } 74 osName = osName.toLowerCase(); 75 // match 76 if (osName.indexOf("windows") != -1) { 77 os = WINDOWS; 78 } else if (osName.indexOf("linux") != -1 || 79 osName.indexOf("sun os") != -1 || 80 osName.indexOf("sunos") != -1 || 81 osName.indexOf("solaris") != -1 || 82 osName.indexOf("mpe/ix") != -1 || 83 osName.indexOf("freebsd") != -1 || 84 osName.indexOf("irix") != -1 || 85 osName.indexOf("digital unix") != -1 || 86 osName.indexOf("unix") != -1 || 87 osName.indexOf("mac os x") != -1) { 88 os = UNIX; 89 } else if (osName.indexOf("hp-ux") != -1 || 90 osName.indexOf("aix") != -1) { 91 os = POSIX_UNIX; 92 } else { 93 os = OTHER; 94 } 95 96 } catch (Exception ex) { 97 os = INIT_PROBLEM; 98 } 99 OS = os; 100 } 101 102 /** 103 * Instances should NOT be constructed in standard programming. 104 */ 105 public FileSystemUtils() { 106 super(); 107 } 108 109 //----------------------------------------------------------------------- 110 /** 111 * Returns the free space on a drive or volume by invoking 112 * the command line. 113 * This method does not normalize the result, and typically returns 114 * bytes on Windows, 512 byte units on OS X and kilobytes on Unix. 115 * As this is not very useful, this method is deprecated in favour 116 * of {@link #freeSpaceKb(String)} which returns a result in kilobytes. 117 * <p> 118 * Note that some OS's are NOT currently supported, including OS/390, 119 * OpenVMS and and SunOS 5. (SunOS is supported by <code>freeSpaceKb</code>.) 120 * <pre> 121 * FileSystemUtils.freeSpace("C:"); // Windows 122 * FileSystemUtils.freeSpace("/volume"); // *nix 123 * </pre> 124 * The free space is calculated via the command line. 125 * It uses 'dir /-c' on Windows and 'df' on *nix. 126 * 127 * @param path the path to get free space for, not null, not empty on Unix 128 * @return the amount of free drive space on the drive or volume 129 * @throws IllegalArgumentException if the path is invalid 130 * @throws IllegalStateException if an error occurred in initialisation 131 * @throws IOException if an error occurs when finding the free space 132 * @since Commons IO 1.1, enhanced OS support in 1.2 and 1.3 133 * @deprecated Use freeSpaceKb(String) 134 * Deprecated from 1.3, may be removed in 2.0 135 */ 136 public static long freeSpace(String path) throws IOException { 137 return INSTANCE.freeSpaceOS(path, OS, false); 138 } 139 140 //----------------------------------------------------------------------- 141 /** 142 * Returns the free space on a drive or volume in kilobytes by invoking 143 * the command line. 144 * <pre> 145 * FileSystemUtils.freeSpaceKb("C:"); // Windows 146 * FileSystemUtils.freeSpaceKb("/volume"); // *nix 147 * </pre> 148 * The free space is calculated via the command line. 149 * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. 150 * <p> 151 * In order to work, you must be running Windows, or have a implementation of 152 * Unix df that supports GNU format when passed -k (or -kP). If you are going 153 * to rely on this code, please check that it works on your OS by running 154 * some simple tests to compare the command line with the output from this class. 155 * If your operating system isn't supported, please raise a JIRA call detailing 156 * the exact result from df -k and as much other detail as possible, thanks. 157 * 158 * @param path the path to get free space for, not null, not empty on Unix 159 * @return the amount of free drive space on the drive or volume in kilobytes 160 * @throws IllegalArgumentException if the path is invalid 161 * @throws IllegalStateException if an error occurred in initialisation 162 * @throws IOException if an error occurs when finding the free space 163 * @since Commons IO 1.2, enhanced OS support in 1.3 164 */ 165 public static long freeSpaceKb(String path) throws IOException { 166 return INSTANCE.freeSpaceOS(path, OS, true); 167 } 168 169 //----------------------------------------------------------------------- 170 /** 171 * Returns the free space on a drive or volume in a cross-platform manner. 172 * Note that some OS's are NOT currently supported, including OS/390. 173 * <pre> 174 * FileSystemUtils.freeSpace("C:"); // Windows 175 * FileSystemUtils.freeSpace("/volume"); // *nix 176 * </pre> 177 * The free space is calculated via the command line. 178 * It uses 'dir /-c' on Windows and 'df' on *nix. 179 * 180 * @param path the path to get free space for, not null, not empty on Unix 181 * @param os the operating system code 182 * @param kb whether to normalize to kilobytes 183 * @return the amount of free drive space on the drive or volume 184 * @throws IllegalArgumentException if the path is invalid 185 * @throws IllegalStateException if an error occurred in initialisation 186 * @throws IOException if an error occurs when finding the free space 187 */ 188 long freeSpaceOS(String path, int os, boolean kb) throws IOException { 189 if (path == null) { 190 throw new IllegalArgumentException("Path must not be empty"); 191 } 192 switch (os) { 193 case WINDOWS: 194 return (kb ? freeSpaceWindows(path) / 1024 : freeSpaceWindows(path)); 195 case UNIX: 196 return freeSpaceUnix(path, kb, false); 197 case POSIX_UNIX: 198 return freeSpaceUnix(path, kb, true); 199 case OTHER: 200 throw new IllegalStateException("Unsupported operating system"); 201 default: 202 throw new IllegalStateException( 203 "Exception caught when determining operating system"); 204 } 205 } 206 207 //----------------------------------------------------------------------- 208 /** 209 * Find free space on the Windows platform using the 'dir' command. 210 * 211 * @param path the path to get free space for, including the colon 212 * @return the amount of free drive space on the drive 213 * @throws IOException if an error occurs 214 */ 215 long freeSpaceWindows(String path) throws IOException { 216 path = FilenameUtils.normalize(path); 217 if (path.length() > 2 && path.charAt(1) == ':') { 218 path = path.substring(0, 2); // seems to make it work 219 } 220 221 // build and run the 'dir' command 222 String[] cmdAttribs = new String[] {"cmd.exe", "/C", "dir /-c " + path}; 223 224 // read in the output of the command to an ArrayList 225 List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE); 226 227 // now iterate over the lines we just read and find the LAST 228 // non-empty line (the free space bytes should be in the last element 229 // of the ArrayList anyway, but this will ensure it works even if it's 230 // not, still assuming it is on the last non-blank line) 231 for (int i = lines.size() - 1; i >= 0; i--) { 232 String line = lines.get(i); 233 if (line.length() > 0) { 234 return parseDir(line, path); 235 } 236 } 237 // all lines are blank 238 throw new IOException( 239 "Command line 'dir /-c' did not return any info " + 240 "for path '" + path + "'"); 241 } 242 243 /** 244 * Parses the Windows dir response last line 245 * 246 * @param line the line to parse 247 * @param path the path that was sent 248 * @return the number of bytes 249 * @throws IOException if an error occurs 250 */ 251 long parseDir(String line, String path) throws IOException { 252 // read from the end of the line to find the last numeric 253 // character on the line, then continue until we find the first 254 // non-numeric character, and everything between that and the last 255 // numeric character inclusive is our free space bytes count 256 int bytesStart = 0; 257 int bytesEnd = 0; 258 int j = line.length() - 1; 259 innerLoop1: while (j >= 0) { 260 char c = line.charAt(j); 261 if (Character.isDigit(c)) { 262 // found the last numeric character, this is the end of 263 // the free space bytes count 264 bytesEnd = j + 1; 265 break innerLoop1; 266 } 267 j--; 268 } 269 innerLoop2: while (j >= 0) { 270 char c = line.charAt(j); 271 if (!Character.isDigit(c) && c != ',' && c != '.') { 272 // found the next non-numeric character, this is the 273 // beginning of the free space bytes count 274 bytesStart = j + 1; 275 break innerLoop2; 276 } 277 j--; 278 } 279 if (j < 0) { 280 throw new IOException( 281 "Command line 'dir /-c' did not return valid info " + 282 "for path '" + path + "'"); 283 } 284 285 // remove commas and dots in the bytes count 286 StringBuffer buf = new StringBuffer(line.substring(bytesStart, bytesEnd)); 287 for (int k = 0; k < buf.length(); k++) { 288 if (buf.charAt(k) == ',' || buf.charAt(k) == '.') { 289 buf.deleteCharAt(k--); 290 } 291 } 292 return parseBytes(buf.toString(), path); 293 } 294 295 //----------------------------------------------------------------------- 296 /** 297 * Find free space on the *nix platform using the 'df' command. 298 * 299 * @param path the path to get free space for 300 * @param kb whether to normalize to kilobytes 301 * @param posix whether to use the posix standard format flag 302 * @return the amount of free drive space on the volume 303 * @throws IOException if an error occurs 304 */ 305 long freeSpaceUnix(String path, boolean kb, boolean posix) throws IOException { 306 if (path.length() == 0) { 307 throw new IllegalArgumentException("Path must not be empty"); 308 } 309 path = FilenameUtils.normalize(path); 310 311 // build and run the 'dir' command 312 String flags = "-"; 313 if (kb) { 314 flags += "k"; 315 } 316 if (posix) { 317 flags += "P"; 318 } 319 String[] cmdAttribs = 320 (flags.length() > 1 ? new String[] {"df", flags, path} : new String[] {"df", path}); 321 322 // perform the command, asking for up to 3 lines (header, interesting, overflow) 323 List<String> lines = performCommand(cmdAttribs, 3); 324 if (lines.size() < 2) { 325 // unknown problem, throw exception 326 throw new IOException( 327 "Command line 'df' did not return info as expected " + 328 "for path '" + path + "'- response was " + lines); 329 } 330 String line2 = lines.get(1); // the line we're interested in 331 332 // Now, we tokenize the string. The fourth element is what we want. 333 StringTokenizer tok = new StringTokenizer(line2, " "); 334 if (tok.countTokens() < 4) { 335 // could be long Filesystem, thus data on third line 336 if (tok.countTokens() == 1 && lines.size() >= 3) { 337 String line3 = lines.get(2); // the line may be interested in 338 tok = new StringTokenizer(line3, " "); 339 } else { 340 throw new IOException( 341 "Command line 'df' did not return data as expected " + 342 "for path '" + path + "'- check path is valid"); 343 } 344 } else { 345 tok.nextToken(); // Ignore Filesystem 346 } 347 tok.nextToken(); // Ignore 1K-blocks 348 tok.nextToken(); // Ignore Used 349 String freeSpace = tok.nextToken(); 350 return parseBytes(freeSpace, path); 351 } 352 353 //----------------------------------------------------------------------- 354 /** 355 * Parses the bytes from a string. 356 * 357 * @param freeSpace the free space string 358 * @param path the path 359 * @return the number of bytes 360 * @throws IOException if an error occurs 361 */ 362 long parseBytes(String freeSpace, String path) throws IOException { 363 try { 364 long bytes = Long.parseLong(freeSpace); 365 if (bytes < 0) { 366 throw new IOException( 367 "Command line 'df' did not find free space in response " + 368 "for path '" + path + "'- check path is valid"); 369 } 370 return bytes; 371 372 } catch (NumberFormatException ex) { 373 throw new IOException( 374 "Command line 'df' did not return numeric data as expected " + 375 "for path '" + path + "'- check path is valid"); 376 } 377 } 378 379 //----------------------------------------------------------------------- 380 /** 381 * Performs the os command. 382 * 383 * @param cmdAttribs the command line parameters 384 * @param max The maximum limit for the lines returned 385 * @return the parsed data 386 * @throws IOException if an error occurs 387 */ 388 List<String> performCommand(String[] cmdAttribs, int max) throws IOException { 389 // this method does what it can to avoid the 'Too many open files' error 390 // based on trial and error and these links: 391 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692 392 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027 393 // http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018 394 // however, its still not perfect as the JDK support is so poor 395 // (see commond-exec or ant for a better multi-threaded multi-os solution) 396 397 List<String> lines = new ArrayList<String>(20); 398 Process proc = null; 399 InputStream in = null; 400 OutputStream out = null; 401 InputStream err = null; 402 BufferedReader inr = null; 403 try { 404 proc = openProcess(cmdAttribs); 405 in = proc.getInputStream(); 406 out = proc.getOutputStream(); 407 err = proc.getErrorStream(); 408 inr = new BufferedReader(new InputStreamReader(in)); 409 String line = inr.readLine(); 410 while (line != null && lines.size() < max) { 411 line = line.toLowerCase().trim(); 412 lines.add(line); 413 line = inr.readLine(); 414 } 415 416 proc.waitFor(); 417 if (proc.exitValue() != 0) { 418 // os command problem, throw exception 419 throw new IOException( 420 "Command line returned OS error code '" + proc.exitValue() + 421 "' for command " + Arrays.asList(cmdAttribs)); 422 } 423 if (lines.size() == 0) { 424 // unknown problem, throw exception 425 throw new IOException( 426 "Command line did not return any info " + 427 "for command " + Arrays.asList(cmdAttribs)); 428 } 429 return lines; 430 431 } catch (InterruptedException ex) { 432 throw new IOException( 433 "Command line threw an InterruptedException '" + ex.getMessage() + 434 "' for command " + Arrays.asList(cmdAttribs)); 435 } finally { 436 IOUtils.closeQuietly(in); 437 IOUtils.closeQuietly(out); 438 IOUtils.closeQuietly(err); 439 IOUtils.closeQuietly(inr); 440 if (proc != null) { 441 proc.destroy(); 442 } 443 } 444 } 445 446 /** 447 * Opens the process to the operating system. 448 * 449 * @param cmdAttribs the command line parameters 450 * @return the process 451 * @throws IOException if an error occurs 452 */ 453 Process openProcess(String[] cmdAttribs) throws IOException { 454 return Runtime.getRuntime().exec(cmdAttribs); 455 } 456 457} 458