• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.nio.charset.Charset;
25 import java.time.Duration;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Objects;
30 import java.util.StringTokenizer;
31 import java.util.stream.Collectors;
32 
33 /**
34  * General File System utilities.
35  * <p>
36  * This class provides static utility methods for general file system
37  * functions not provided via the JDK {@link java.io.File File} class.
38  * <p>
39  * The current functions provided are:
40  * <ul>
41  * <li>Get the free space on a drive
42  * </ul>
43  *
44  * @since 1.1
45  * @deprecated As of 2.6 deprecated without replacement. Use equivalent
46  *  methods in {@link java.nio.file.FileStore} instead, e.g.
47  *  {@code Files.getFileStore(Paths.get("/home")).getUsableSpace()}
48  *  or iterate over {@code FileSystems.getDefault().getFileStores()}
49  */
50 @Deprecated
51 public class FileSystemUtils {
52 
53     /** Singleton instance, used mainly for testing. */
54     private static final FileSystemUtils INSTANCE = new FileSystemUtils();
55 
56     /** Operating system state flag for error. */
57     private static final int INIT_PROBLEM = -1;
58     /** Operating system state flag for neither Unix nor Windows. */
59     private static final int OTHER = 0;
60     /** Operating system state flag for Windows. */
61     private static final int WINDOWS = 1;
62     /** Operating system state flag for Unix. */
63     private static final int UNIX = 2;
64     /** Operating system state flag for Posix flavour Unix. */
65     private static final int POSIX_UNIX = 3;
66 
67     /** The operating system flag. */
68     private static final int OS;
69 
70     /** The path to df */
71     private static final String DF;
72 
73     static {
74         int os = OTHER;
75         String dfPath = "df";
76         try {
77             String osName = System.getProperty("os.name");
78             if (osName == null) {
79                 throw new IOException("os.name not found");
80             }
81             osName = osName.toLowerCase(Locale.ENGLISH);
82             // match
83             if (osName.contains("windows")) {
84                 os = WINDOWS;
85             } else if (osName.contains("linux") ||
86                     osName.contains("mpe/ix") ||
87                     osName.contains("freebsd") ||
88                     osName.contains("openbsd") ||
89                     osName.contains("irix") ||
90                     osName.contains("digital unix") ||
91                     osName.contains("unix") ||
92                     osName.contains("mac os x")) {
93                 os = UNIX;
94             } else if (osName.contains("sun os") ||
95                     osName.contains("sunos") ||
96                     osName.contains("solaris")) {
97                 os = POSIX_UNIX;
98                 dfPath = "/usr/xpg4/bin/df";
99             } else if (osName.contains("hp-ux") ||
100                     osName.contains("aix")) {
101                 os = POSIX_UNIX;
102             }
103 
104         } catch (final Exception ex) {
105             os = INIT_PROBLEM;
106         }
107         OS = os;
108         DF = dfPath;
109     }
110 
111     /**
112      * Returns the free space on a drive or volume by invoking
113      * the command line.
114      * This method does not normalize the result, and typically returns
115      * bytes on Windows, 512 byte units on OS X and kilobytes on Unix.
116      * As this is not very useful, this method is deprecated in favour
117      * of {@link #freeSpaceKb(String)} which returns a result in kilobytes.
118      * <p>
119      * Note that some OS's are NOT currently supported, including OS/390,
120      * OpenVMS.
121      * <pre>
122      * FileSystemUtils.freeSpace("C:");       // Windows
123      * FileSystemUtils.freeSpace("/volume");  // *nix
124      * </pre>
125      * The free space is calculated via the command line.
126      * It uses 'dir /-c' on Windows and 'df' on *nix.
127      *
128      * @param path  the path to get free space for, not null, not empty on Unix
129      * @return the amount of free drive space on the drive or volume
130      * @throws IllegalArgumentException if the path is invalid
131      * @throws IllegalStateException if an error occurred in initialisation
132      * @throws IOException if an error occurs when finding the free space
133      * @since 1.1, enhanced OS support in 1.2 and 1.3
134      * @deprecated Use freeSpaceKb(String)
135      *  Deprecated from 1.3, may be removed in 2.0
136      */
137     @Deprecated
freeSpace(final String path)138     public static long freeSpace(final String path) throws IOException {
139         return INSTANCE.freeSpaceOS(path, OS, false, Duration.ofMillis(-1));
140     }
141 
142     /**
143      * Returns the free space for the working directory
144      * in kibibytes (1024 bytes) by invoking the command line.
145      * <p>
146      * Identical to:
147      * <pre>
148      * freeSpaceKb(FileUtils.current().getAbsolutePath())
149      * </pre>
150      * @return the amount of free drive space on the drive or volume in kilobytes
151      * @throws IllegalStateException if an error occurred in initialisation
152      * @throws IOException if an error occurs when finding the free space
153      * @since 2.0
154      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
155      */
156     @Deprecated
freeSpaceKb()157     public static long freeSpaceKb() throws IOException {
158         return freeSpaceKb(-1);
159     }
160 
161     /**
162      * Returns the free space for the working directory
163      * in kibibytes (1024 bytes) by invoking the command line.
164      * <p>
165      * Identical to:
166      * <pre>
167      * freeSpaceKb(FileUtils.current().getAbsolutePath())
168      * </pre>
169      * @param timeout The timeout amount in milliseconds or no timeout if the value
170      *  is zero or less
171      * @return the amount of free drive space on the drive or volume in kilobytes
172      * @throws IllegalStateException if an error occurred in initialisation
173      * @throws IOException if an error occurs when finding the free space
174      * @since 2.0
175      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
176      */
177     @Deprecated
freeSpaceKb(final long timeout)178     public static long freeSpaceKb(final long timeout) throws IOException {
179         return freeSpaceKb(FileUtils.current().getAbsolutePath(), timeout);
180     }
181     /**
182      * Returns the free space on a drive or volume in kibibytes (1024 bytes)
183      * by invoking the command line.
184      * <pre>
185      * FileSystemUtils.freeSpaceKb("C:");       // Windows
186      * FileSystemUtils.freeSpaceKb("/volume");  // *nix
187      * </pre>
188      * The free space is calculated via the command line.
189      * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
190      * <p>
191      * In order to work, you must be running Windows, or have an implementation of
192      * Unix df that supports GNU format when passed -k (or -kP). If you are going
193      * to rely on this code, please check that it works on your OS by running
194      * some simple tests to compare the command line with the output from this class.
195      * If your operating system isn't supported, please raise a JIRA call detailing
196      * the exact result from df -k and as much other detail as possible, thanks.
197      *
198      * @param path  the path to get free space for, not null, not empty on Unix
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 1.2, enhanced OS support in 1.3
204      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
205      */
206     @Deprecated
freeSpaceKb(final String path)207     public static long freeSpaceKb(final String path) throws IOException {
208         return freeSpaceKb(path, -1);
209     }
210 
211     /**
212      * Returns the free space on a drive or volume in kibibytes (1024 bytes)
213      * by invoking the command line.
214      * <pre>
215      * FileSystemUtils.freeSpaceKb("C:");       // Windows
216      * FileSystemUtils.freeSpaceKb("/volume");  // *nix
217      * </pre>
218      * The free space is calculated via the command line.
219      * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
220      * <p>
221      * In order to work, you must be running Windows, or have an implementation of
222      * Unix df that supports GNU format when passed -k (or -kP). If you are going
223      * to rely on this code, please check that it works on your OS by running
224      * some simple tests to compare the command line with the output from this class.
225      * If your operating system isn't supported, please raise a JIRA call detailing
226      * the exact result from df -k and as much other detail as possible, thanks.
227      *
228      * @param path  the path to get free space for, not null, not empty on Unix
229      * @param timeout The timeout amount in milliseconds or no timeout if the value
230      *  is zero or less
231      * @return the amount of free drive space on the drive or volume in kilobytes
232      * @throws IllegalArgumentException if the path is invalid
233      * @throws IllegalStateException if an error occurred in initialisation
234      * @throws IOException if an error occurs when finding the free space
235      * @since 2.0
236      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
237      */
238     @Deprecated
freeSpaceKb(final String path, final long timeout)239     public static long freeSpaceKb(final String path, final long timeout) throws IOException {
240         return INSTANCE.freeSpaceOS(path, OS, true, Duration.ofMillis(timeout));
241     }
242 
243     /**
244      * Instances should NOT be constructed in standard programming.
245      */
FileSystemUtils()246     public FileSystemUtils() {
247     }
248 
249     /**
250      * Returns the free space on a drive or volume in a cross-platform manner.
251      * Note that some OS's are NOT currently supported, including OS/390.
252      * <pre>
253      * FileSystemUtils.freeSpace("C:");  // Windows
254      * FileSystemUtils.freeSpace("/volume");  // *nix
255      * </pre>
256      * The free space is calculated via the command line.
257      * It uses 'dir /-c' on Windows and 'df' on *nix.
258      *
259      * @param path  the path to get free space for, not null, not empty on Unix
260      * @param os  the operating system code
261      * @param kb  whether to normalize to kilobytes
262      * @param timeout The timeout amount in milliseconds or no timeout if the value
263      *  is zero or less
264      * @return the amount of free drive space on the drive or volume
265      * @throws IllegalArgumentException if the path is invalid
266      * @throws IllegalStateException if an error occurred in initialization
267      * @throws IOException if an error occurs when finding the free space
268      */
freeSpaceOS(final String path, final int os, final boolean kb, final Duration timeout)269     long freeSpaceOS(final String path, final int os, final boolean kb, final Duration timeout) throws IOException {
270         Objects.requireNonNull(path, "path");
271         switch (os) {
272         case WINDOWS:
273             return kb ? freeSpaceWindows(path, timeout) / FileUtils.ONE_KB : freeSpaceWindows(path, timeout);
274         case UNIX:
275             return freeSpaceUnix(path, kb, false, timeout);
276         case POSIX_UNIX:
277             return freeSpaceUnix(path, kb, true, timeout);
278         case OTHER:
279             throw new IllegalStateException("Unsupported operating system");
280         default:
281             throw new IllegalStateException("Exception caught when determining operating system");
282         }
283     }
284 
285     /**
286      * Find free space on the *nix platform using the 'df' command.
287      *
288      * @param path  the path to get free space for
289      * @param kb  whether to normalize to kilobytes
290      * @param posix  whether to use the POSIX standard format flag
291      * @param timeout The timeout amount in milliseconds or no timeout if the value
292      *  is zero or less
293      * @return the amount of free drive space on the volume
294      * @throws IOException if an error occurs
295      */
freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout)296     long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout)
297             throws IOException {
298         if (path.isEmpty()) {
299             throw new IllegalArgumentException("Path must not be empty");
300         }
301 
302         // build and run the 'dir' command
303         String flags = "-";
304         if (kb) {
305             flags += "k";
306         }
307         if (posix) {
308             flags += "P";
309         }
310         final String[] cmdAttribs =
311             flags.length() > 1 ? new String[] {DF, flags, path} : new String[] {DF, path};
312 
313         // perform the command, asking for up to 3 lines (header, interesting, overflow)
314         final List<String> lines = performCommand(cmdAttribs, 3, timeout);
315         if (lines.size() < 2) {
316             // unknown problem, throw exception
317             throw new IOException(
318                     "Command line '" + DF + "' did not return info as expected " +
319                     "for path '" + path + "'- response was " + lines);
320         }
321         final String line2 = lines.get(1); // the line we're interested in
322 
323         // Now, we tokenize the string. The fourth element is what we want.
324         StringTokenizer tok = new StringTokenizer(line2, " ");
325         if (tok.countTokens() < 4) {
326             // could be long Filesystem, thus data on third line
327             if (tok.countTokens() != 1 || lines.size() < 3) {
328                 throw new IOException(
329                         "Command line '" + DF + "' did not return data as expected " +
330                         "for path '" + path + "'- check path is valid");
331             }
332             final String line3 = lines.get(2); // the line may be interested in
333             tok = new StringTokenizer(line3, " ");
334         } else {
335             tok.nextToken(); // Ignore Filesystem
336         }
337         tok.nextToken(); // Ignore 1K-blocks
338         tok.nextToken(); // Ignore Used
339         final String freeSpace = tok.nextToken();
340         return parseBytes(freeSpace, path);
341     }
342 
343     /**
344      * Find free space on the Windows platform using the 'dir' command.
345      *
346      * @param path  the path to get free space for, including the colon
347      * @param timeout The timeout amount in milliseconds or no timeout if the value
348      *  is zero or less
349      * @return the amount of free drive space on the drive
350      * @throws IOException if an error occurs
351      */
freeSpaceWindows(final String path, final Duration timeout)352     long freeSpaceWindows(final String path, final Duration timeout) throws IOException {
353         String normPath = FilenameUtils.normalize(path, false);
354         if (normPath == null) {
355             throw new IllegalArgumentException(path);
356         }
357         if (!normPath.isEmpty() && normPath.charAt(0) != '"') {
358             normPath = "\"" + normPath + "\"";
359         }
360 
361         // build and run the 'dir' command
362         final String[] cmdAttribs = {"cmd.exe", "/C", "dir /a /-c " + normPath};
363 
364         // read in the output of the command to an ArrayList
365         final List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE, timeout);
366 
367         // now iterate over the lines we just read and find the LAST
368         // non-empty line (the free space bytes should be in the last element
369         // of the ArrayList anyway, but this will ensure it works even if it's
370         // not, still assuming it is on the last non-blank line)
371         for (int i = lines.size() - 1; i >= 0; i--) {
372             final String line = lines.get(i);
373             if (!line.isEmpty()) {
374                 return parseDir(line, normPath);
375             }
376         }
377         // all lines are blank
378         throw new IOException(
379                 "Command line 'dir /-c' did not return any info " +
380                 "for path '" + normPath + "'");
381     }
382 
383     /**
384      * Opens the process to the operating system.
385      *
386      * @param cmdAttribs  the command line parameters
387      * @return the process
388      * @throws IOException if an error occurs
389      */
openProcess(final String[] cmdAttribs)390     Process openProcess(final String[] cmdAttribs) throws IOException {
391         return Runtime.getRuntime().exec(cmdAttribs);
392     }
393 
394     /**
395      * Parses the bytes from a string.
396      *
397      * @param freeSpace  the free space string
398      * @param path  the path
399      * @return the number of bytes
400      * @throws IOException if an error occurs
401      */
parseBytes(final String freeSpace, final String path)402     long parseBytes(final String freeSpace, final String path) throws IOException {
403         try {
404             final long bytes = Long.parseLong(freeSpace);
405             if (bytes < 0) {
406                 throw new IOException(
407                         "Command line '" + DF + "' did not find free space in response " +
408                         "for path '" + path + "'- check path is valid");
409             }
410             return bytes;
411 
412         } catch (final NumberFormatException ex) {
413             throw new IOException(
414                     "Command line '" + DF + "' did not return numeric data as expected " +
415                     "for path '" + path + "'- check path is valid", ex);
416         }
417     }
418 
419     /**
420      * Parses the Windows dir response last line
421      *
422      * @param line  the line to parse
423      * @param path  the path that was sent
424      * @return the number of bytes
425      * @throws IOException if an error occurs
426      */
parseDir(final String line, final String path)427     long parseDir(final String line, final String path) throws IOException {
428         // read from the end of the line to find the last numeric
429         // character on the line, then continue until we find the first
430         // non-numeric character, and everything between that and the last
431         // numeric character inclusive is our free space bytes count
432         int bytesStart = 0;
433         int bytesEnd = 0;
434         int j = line.length() - 1;
435         innerLoop1: while (j >= 0) {
436             final char c = line.charAt(j);
437             if (Character.isDigit(c)) {
438               // found the last numeric character, this is the end of
439               // the free space bytes count
440               bytesEnd = j + 1;
441               break innerLoop1;
442             }
443             j--;
444         }
445         innerLoop2: while (j >= 0) {
446             final char c = line.charAt(j);
447             if (!Character.isDigit(c) && c != ',' && c != '.') {
448               // found the next non-numeric character, this is the
449               // beginning of the free space bytes count
450               bytesStart = j + 1;
451               break innerLoop2;
452             }
453             j--;
454         }
455         if (j < 0) {
456             throw new IOException(
457                     "Command line 'dir /-c' did not return valid info " +
458                     "for path '" + path + "'");
459         }
460 
461         // remove commas and dots in the bytes count
462         final StringBuilder buf = new StringBuilder(line.substring(bytesStart, bytesEnd));
463         for (int k = 0; k < buf.length(); k++) {
464             if (buf.charAt(k) == ',' || buf.charAt(k) == '.') {
465                 buf.deleteCharAt(k--);
466             }
467         }
468         return parseBytes(buf.toString(), path);
469     }
470 
471     /**
472      * Performs an OS command.
473      *
474      * @param cmdAttribs  the command line parameters
475      * @param max The maximum limit for the lines returned
476      * @param timeout The timeout amount in milliseconds or no timeout if the value
477      *  is zero or less
478      * @return the lines returned by the command, converted to lower-case
479      * @throws IOException if an error occurs
480      */
performCommand(final String[] cmdAttribs, final int max, final Duration timeout)481     List<String> performCommand(final String[] cmdAttribs, final int max, final Duration timeout) throws IOException {
482         // this method does what it can to avoid the 'Too many open files' error
483         // based on trial and error and these links:
484         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
485         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027
486         // http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018
487         // however, it's still not perfect as the JDK support is so poor
488         // (see commons-exec or Ant for a better multithreaded multi-os solution)
489 
490         final List<String> lines;
491         Process proc = null;
492         InputStream in = null;
493         OutputStream out = null;
494         InputStream err = null;
495         BufferedReader inr = null;
496         try {
497 
498             final Thread monitor = ThreadMonitor.start(timeout);
499 
500             proc = openProcess(cmdAttribs);
501             in = proc.getInputStream();
502             out = proc.getOutputStream();
503             err = proc.getErrorStream();
504             // default charset is most likely appropriate here
505             inr = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()));
506 
507             lines = inr.lines().limit(max).map(line -> line.toLowerCase(Locale.ENGLISH).trim()).collect(Collectors.toList());
508 
509             proc.waitFor();
510 
511             ThreadMonitor.stop(monitor);
512 
513             if (proc.exitValue() != 0) {
514                 // OS command problem, throw exception
515                 throw new IOException("Command line returned OS error code '" + proc.exitValue() + "' for command " + Arrays.asList(cmdAttribs));
516             }
517             if (lines.isEmpty()) {
518                 // unknown problem, throw exception
519                 throw new IOException("Command line did not return any info " + "for command " + Arrays.asList(cmdAttribs));
520             }
521 
522             inr.close();
523             inr = null;
524 
525             in.close();
526             in = null;
527 
528             if (out != null) {
529                 out.close();
530                 out = null;
531             }
532 
533             if (err != null) {
534                 err.close();
535                 err = null;
536             }
537 
538             return lines;
539 
540         } catch (final InterruptedException ex) {
541             throw new IOException(
542                     "Command line threw an InterruptedException " +
543                     "for command " + Arrays.asList(cmdAttribs) + " timeout=" + timeout, ex);
544         } finally {
545             IOUtils.closeQuietly(in);
546             IOUtils.closeQuietly(out);
547             IOUtils.closeQuietly(err);
548             IOUtils.closeQuietly(inr);
549             if (proc != null) {
550                 proc.destroy();
551             }
552         }
553     }
554 
555 }
556