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 }