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.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.List; 27 import 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 */ 49 public 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 @Deprecated 137 public static long freeSpace(String path) throws IOException { 138 return INSTANCE.freeSpaceOS(path, OS, false); 139 } 140 141 //----------------------------------------------------------------------- 142 /** 143 * Returns the free space on a drive or volume in kilobytes by invoking 144 * the command line. 145 * <pre> 146 * FileSystemUtils.freeSpaceKb("C:"); // Windows 147 * FileSystemUtils.freeSpaceKb("/volume"); // *nix 148 * </pre> 149 * The free space is calculated via the command line. 150 * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. 151 * <p> 152 * In order to work, you must be running Windows, or have a implementation of 153 * Unix df that supports GNU format when passed -k (or -kP). If you are going 154 * to rely on this code, please check that it works on your OS by running 155 * some simple tests to compare the command line with the output from this class. 156 * If your operating system isn't supported, please raise a JIRA call detailing 157 * the exact result from df -k and as much other detail as possible, thanks. 158 * 159 * @param path the path to get free space for, not null, not empty on Unix 160 * @return the amount of free drive space on the drive or volume in kilobytes 161 * @throws IllegalArgumentException if the path is invalid 162 * @throws IllegalStateException if an error occurred in initialisation 163 * @throws IOException if an error occurs when finding the free space 164 * @since Commons IO 1.2, enhanced OS support in 1.3 165 */ 166 public static long freeSpaceKb(String path) throws IOException { 167 return INSTANCE.freeSpaceOS(path, OS, true); 168 } 169 170 //----------------------------------------------------------------------- 171 /** 172 * Returns the free space on a drive or volume in a cross-platform manner. 173 * Note that some OS's are NOT currently supported, including OS/390. 174 * <pre> 175 * FileSystemUtils.freeSpace("C:"); // Windows 176 * FileSystemUtils.freeSpace("/volume"); // *nix 177 * </pre> 178 * The free space is calculated via the command line. 179 * It uses 'dir /-c' on Windows and 'df' on *nix. 180 * 181 * @param path the path to get free space for, not null, not empty on Unix 182 * @param os the operating system code 183 * @param kb whether to normalize to kilobytes 184 * @return the amount of free drive space on the drive or volume 185 * @throws IllegalArgumentException if the path is invalid 186 * @throws IllegalStateException if an error occurred in initialisation 187 * @throws IOException if an error occurs when finding the free space 188 */ 189 long freeSpaceOS(String path, int os, boolean kb) throws IOException { 190 if (path == null) { 191 throw new IllegalArgumentException("Path must not be empty"); 192 } 193 switch (os) { 194 case WINDOWS: 195 return (kb ? freeSpaceWindows(path) / 1024 : freeSpaceWindows(path)); 196 case UNIX: 197 return freeSpaceUnix(path, kb, false); 198 case POSIX_UNIX: 199 return freeSpaceUnix(path, kb, true); 200 case OTHER: 201 throw new IllegalStateException("Unsupported operating system"); 202 default: 203 throw new IllegalStateException( 204 "Exception caught when determining operating system"); 205 } 206 } 207 208 //----------------------------------------------------------------------- 209 /** 210 * Find free space on the Windows platform using the 'dir' command. 211 * 212 * @param path the path to get free space for, including the colon 213 * @return the amount of free drive space on the drive 214 * @throws IOException if an error occurs 215 */ 216 long freeSpaceWindows(String path) throws IOException { 217 path = FilenameUtils.normalize(path); 218 if (path.length() > 2 && path.charAt(1) == ':') { 219 path = path.substring(0, 2); // seems to make it work 220 } 221 222 // build and run the 'dir' command 223 String[] cmdAttribs = new String[] {"cmd.exe", "/C", "dir /-c " + path}; 224 225 // read in the output of the command to an ArrayList 226 List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE); 227 228 // now iterate over the lines we just read and find the LAST 229 // non-empty line (the free space bytes should be in the last element 230 // of the ArrayList anyway, but this will ensure it works even if it's 231 // not, still assuming it is on the last non-blank line) 232 for (int i = lines.size() - 1; i >= 0; i--) { 233 String line = lines.get(i); 234 if (line.length() > 0) { 235 return parseDir(line, path); 236 } 237 } 238 // all lines are blank 239 throw new IOException( 240 "Command line 'dir /-c' did not return any info " + 241 "for path '" + path + "'"); 242 } 243 244 /** 245 * Parses the Windows dir response last line 246 * 247 * @param line the line to parse 248 * @param path the path that was sent 249 * @return the number of bytes 250 * @throws IOException if an error occurs 251 */ 252 long parseDir(String line, String path) throws IOException { 253 // read from the end of the line to find the last numeric 254 // character on the line, then continue until we find the first 255 // non-numeric character, and everything between that and the last 256 // numeric character inclusive is our free space bytes count 257 int bytesStart = 0; 258 int bytesEnd = 0; 259 int j = line.length() - 1; 260 innerLoop1: while (j >= 0) { 261 char c = line.charAt(j); 262 if (Character.isDigit(c)) { 263 // found the last numeric character, this is the end of 264 // the free space bytes count 265 bytesEnd = j + 1; 266 break innerLoop1; 267 } 268 j--; 269 } 270 innerLoop2: while (j >= 0) { 271 char c = line.charAt(j); 272 if (!Character.isDigit(c) && c != ',' && c != '.') { 273 // found the next non-numeric character, this is the 274 // beginning of the free space bytes count 275 bytesStart = j + 1; 276 break innerLoop2; 277 } 278 j--; 279 } 280 if (j < 0) { 281 throw new IOException( 282 "Command line 'dir /-c' did not return valid info " + 283 "for path '" + path + "'"); 284 } 285 286 // remove commas and dots in the bytes count 287 StringBuffer buf = new StringBuffer(line.substring(bytesStart, bytesEnd)); 288 for (int k = 0; k < buf.length(); k++) { 289 if (buf.charAt(k) == ',' || buf.charAt(k) == '.') { 290 buf.deleteCharAt(k--); 291 } 292 } 293 return parseBytes(buf.toString(), path); 294 } 295 296 //----------------------------------------------------------------------- 297 /** 298 * Find free space on the *nix platform using the 'df' command. 299 * 300 * @param path the path to get free space for 301 * @param kb whether to normalize to kilobytes 302 * @param posix whether to use the posix standard format flag 303 * @return the amount of free drive space on the volume 304 * @throws IOException if an error occurs 305 */ 306 long freeSpaceUnix(String path, boolean kb, boolean posix) throws IOException { 307 if (path.length() == 0) { 308 throw new IllegalArgumentException("Path must not be empty"); 309 } 310 path = FilenameUtils.normalize(path); 311 312 // build and run the 'dir' command 313 String flags = "-"; 314 if (kb) { 315 flags += "k"; 316 } 317 if (posix) { 318 flags += "P"; 319 } 320 String[] cmdAttribs = 321 (flags.length() > 1 ? new String[] {"df", flags, path} : new String[] {"df", path}); 322 323 // perform the command, asking for up to 3 lines (header, interesting, overflow) 324 List<String> lines = performCommand(cmdAttribs, 3); 325 if (lines.size() < 2) { 326 // unknown problem, throw exception 327 throw new IOException( 328 "Command line 'df' did not return info as expected " + 329 "for path '" + path + "'- response was " + lines); 330 } 331 String line2 = lines.get(1); // the line we're interested in 332 333 // Now, we tokenize the string. The fourth element is what we want. 334 StringTokenizer tok = new StringTokenizer(line2, " "); 335 if (tok.countTokens() < 4) { 336 // could be long Filesystem, thus data on third line 337 if (tok.countTokens() == 1 && lines.size() >= 3) { 338 String line3 = lines.get(2); // the line may be interested in 339 tok = new StringTokenizer(line3, " "); 340 } else { 341 throw new IOException( 342 "Command line 'df' did not return data as expected " + 343 "for path '" + path + "'- check path is valid"); 344 } 345 } else { 346 tok.nextToken(); // Ignore Filesystem 347 } 348 tok.nextToken(); // Ignore 1K-blocks 349 tok.nextToken(); // Ignore Used 350 String freeSpace = tok.nextToken(); 351 return parseBytes(freeSpace, path); 352 } 353 354 //----------------------------------------------------------------------- 355 /** 356 * Parses the bytes from a string. 357 * 358 * @param freeSpace the free space string 359 * @param path the path 360 * @return the number of bytes 361 * @throws IOException if an error occurs 362 */ 363 long parseBytes(String freeSpace, String path) throws IOException { 364 try { 365 long bytes = Long.parseLong(freeSpace); 366 if (bytes < 0) { 367 throw new IOException( 368 "Command line 'df' did not find free space in response " + 369 "for path '" + path + "'- check path is valid"); 370 } 371 return bytes; 372 373 } catch (NumberFormatException ex) { 374 throw new IOException( 375 "Command line 'df' did not return numeric data as expected " + 376 "for path '" + path + "'- check path is valid"); 377 } 378 } 379 380 //----------------------------------------------------------------------- 381 /** 382 * Performs the os command. 383 * 384 * @param cmdAttribs the command line parameters 385 * @param max The maximum limit for the lines returned 386 * @return the parsed data 387 * @throws IOException if an error occurs 388 */ 389 List<String> performCommand(String[] cmdAttribs, int max) throws IOException { 390 // this method does what it can to avoid the 'Too many open files' error 391 // based on trial and error and these links: 392 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692 393 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027 394 // http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018 395 // however, its still not perfect as the JDK support is so poor 396 // (see commond-exec or ant for a better multi-threaded multi-os solution) 397 398 List<String> lines = new ArrayList<String>(20); 399 Process proc = null; 400 InputStream in = null; 401 OutputStream out = null; 402 InputStream err = null; 403 BufferedReader inr = null; 404 try { 405 proc = openProcess(cmdAttribs); 406 in = proc.getInputStream(); 407 out = proc.getOutputStream(); 408 err = proc.getErrorStream(); 409 inr = new BufferedReader(new InputStreamReader(in)); 410 String line = inr.readLine(); 411 while (line != null && lines.size() < max) { 412 line = line.toLowerCase().trim(); 413 lines.add(line); 414 line = inr.readLine(); 415 } 416 417 proc.waitFor(); 418 if (proc.exitValue() != 0) { 419 // os command problem, throw exception 420 throw new IOException( 421 "Command line returned OS error code '" + proc.exitValue() + 422 "' for command " + Arrays.asList(cmdAttribs)); 423 } 424 if (lines.size() == 0) { 425 // unknown problem, throw exception 426 throw new IOException( 427 "Command line did not return any info " + 428 "for command " + Arrays.asList(cmdAttribs)); 429 } 430 return lines; 431 432 } catch (InterruptedException ex) { 433 throw new IOException( 434 "Command line threw an InterruptedException '" + ex.getMessage() + 435 "' for command " + Arrays.asList(cmdAttribs)); 436 } finally { 437 IOUtils.closeQuietly(in); 438 IOUtils.closeQuietly(out); 439 IOUtils.closeQuietly(err); 440 IOUtils.closeQuietly(inr); 441 if (proc != null) { 442 proc.destroy(); 443 } 444 } 445 } 446 447 /** 448 * Opens the process to the operating system. 449 * 450 * @param cmdAttribs the command line parameters 451 * @return the process 452 * @throws IOException if an error occurs 453 */ 454 Process openProcess(String[] cmdAttribs) throws IOException { 455 return Runtime.getRuntime().exec(cmdAttribs); 456 } 457 458 } 459