• 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 favor
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 initialization
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 initialization
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 initialization
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 initialization
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 initialization
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 I/O 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 = flags.length() > 1 ? new String[] { DF, flags, path } : new String[] { DF, path };
311 
312         // perform the command, asking for up to 3 lines (header, interesting, overflow)
313         final List<String> lines = performCommand(cmdAttribs, 3, timeout);
314         if (lines.size() < 2) {
315             // unknown problem, throw exception
316             throw new IOException("Command line '" + DF + "' did not return info as expected for path '" + path + "'- response was " + lines);
317         }
318         final String line2 = lines.get(1); // the line we're interested in
319 
320         // Now, we tokenize the string. The fourth element is what we want.
321         StringTokenizer tok = new StringTokenizer(line2, " ");
322         if (tok.countTokens() < 4) {
323             // could be long Filesystem, thus data on third line
324             if (tok.countTokens() != 1 || lines.size() < 3) {
325                 throw new IOException("Command line '" + DF + "' did not return data as expected for path '" + path + "'- check path is valid");
326             }
327             final String line3 = lines.get(2); // the line may be interested in
328             tok = new StringTokenizer(line3, " ");
329         } else {
330             tok.nextToken(); // Ignore Filesystem
331         }
332         tok.nextToken(); // Ignore 1K-blocks
333         tok.nextToken(); // Ignore Used
334         final String freeSpace = tok.nextToken();
335         return parseBytes(freeSpace, path);
336     }
337 
338     /**
339      * Find free space on the Windows platform using the 'dir' command.
340      *
341      * @param path  the path to get free space for, including the colon
342      * @param timeout The timeout amount in milliseconds or no timeout if the value
343      *  is zero or less
344      * @return the amount of free drive space on the drive
345      * @throws IOException If an I/O error occurs
346      */
freeSpaceWindows(final String path, final Duration timeout)347     long freeSpaceWindows(final String path, final Duration timeout) throws IOException {
348         String normPath = FilenameUtils.normalize(path, false);
349         if (normPath == null) {
350             throw new IllegalArgumentException(path);
351         }
352         if (!normPath.isEmpty() && normPath.charAt(0) != '"') {
353             normPath = "\"" + normPath + "\"";
354         }
355 
356         // build and run the 'dir' command
357         final String[] cmdAttribs = { "cmd.exe", "/C", "dir /a /-c " + normPath };
358 
359         // read in the output of the command to an ArrayList
360         final List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE, timeout);
361 
362         // now iterate over the lines we just read and find the LAST
363         // non-empty line (the free space bytes should be in the last element
364         // of the ArrayList anyway, but this will ensure it works even if it's
365         // not, still assuming it is on the last non-blank line)
366         for (int i = lines.size() - 1; i >= 0; i--) {
367             final String line = lines.get(i);
368             if (!line.isEmpty()) {
369                 return parseDir(line, normPath);
370             }
371         }
372         // all lines are blank
373         throw new IOException("Command line 'dir /-c' did not return any info for path '" + normPath + "'");
374     }
375 
376     /**
377      * Opens the process to the operating system.
378      *
379      * @param cmdAttribs  the command line parameters
380      * @return the process
381      * @throws IOException If an I/O error occurs
382      */
openProcess(final String[] cmdAttribs)383     Process openProcess(final String[] cmdAttribs) throws IOException {
384         return Runtime.getRuntime().exec(cmdAttribs);
385     }
386 
387     /**
388      * Parses the bytes from a string.
389      *
390      * @param freeSpace  the free space string
391      * @param path  the path
392      * @return the number of bytes
393      * @throws IOException If an I/O error occurs
394      */
parseBytes(final String freeSpace, final String path)395     long parseBytes(final String freeSpace, final String path) throws IOException {
396         try {
397             final long bytes = Long.parseLong(freeSpace);
398             if (bytes < 0) {
399                 throw new IOException("Command line '" + DF + "' did not find free space in response for path '" + path + "'- check path is valid");
400             }
401             return bytes;
402 
403         } catch (final NumberFormatException ex) {
404             throw new IOException("Command line '" + DF + "' did not return numeric data as expected for path '" + path + "'- check path is valid", ex);
405         }
406     }
407 
408     /**
409      * Parses the Windows dir response last line
410      *
411      * @param line  the line to parse
412      * @param path  the path that was sent
413      * @return the number of bytes
414      * @throws IOException If an I/O error occurs
415      */
parseDir(final String line, final String path)416     long parseDir(final String line, final String path) throws IOException {
417         // read from the end of the line to find the last numeric
418         // character on the line, then continue until we find the first
419         // non-numeric character, and everything between that and the last
420         // numeric character inclusive is our free space bytes count
421         int bytesStart = 0;
422         int bytesEnd = 0;
423         int j = line.length() - 1;
424         innerLoop1: while (j >= 0) {
425             final char c = line.charAt(j);
426             if (Character.isDigit(c)) {
427                 // found the last numeric character, this is the end of
428                 // the free space bytes count
429                 bytesEnd = j + 1;
430                 break innerLoop1;
431             }
432             j--;
433         }
434         innerLoop2: while (j >= 0) {
435             final char c = line.charAt(j);
436             if (!Character.isDigit(c) && c != ',' && c != '.') {
437                 // found the next non-numeric character, this is the
438                 // beginning of the free space bytes count
439                 bytesStart = j + 1;
440                 break innerLoop2;
441             }
442             j--;
443         }
444         if (j < 0) {
445             throw new IOException("Command line 'dir /-c' did not return valid info for path '" + path + "'");
446         }
447 
448         // remove commas and dots in the bytes count
449         final StringBuilder buf = new StringBuilder(line.substring(bytesStart, bytesEnd));
450         for (int k = 0; k < buf.length(); k++) {
451             if (buf.charAt(k) == ',' || buf.charAt(k) == '.') {
452                 buf.deleteCharAt(k--);
453             }
454         }
455         return parseBytes(buf.toString(), path);
456     }
457 
458     /**
459      * Performs an OS command.
460      *
461      * @param cmdAttribs  the command line parameters
462      * @param max The maximum limit for the lines returned
463      * @param timeout The timeout amount in milliseconds or no timeout if the value
464      *  is zero or less
465      * @return the lines returned by the command, converted to lower-case
466      * @throws IOException if an error occurs
467      */
performCommand(final String[] cmdAttribs, final int max, final Duration timeout)468     List<String> performCommand(final String[] cmdAttribs, final int max, final Duration timeout) throws IOException {
469         //
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, it's still not perfect as the JDK support is so poor
476         // (see commons-exec or Ant for a better multithreaded multi-OS solution)
477         //
478         final Process proc = openProcess(cmdAttribs);
479         final Thread monitor = ThreadMonitor.start(timeout);
480         try (InputStream in = proc.getInputStream();
481                 OutputStream out = proc.getOutputStream();
482                 // default Charset is most likely appropriate here
483                 InputStream err = proc.getErrorStream();
484                 // If in is null here, InputStreamReader throws NullPointerException
485                 BufferedReader inr = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
486 
487             final List<String> lines = inr.lines().limit(max).map(line -> line.toLowerCase(Locale.getDefault()).trim()).collect(Collectors.toList());
488             proc.waitFor();
489             ThreadMonitor.stop(monitor);
490 
491             if (proc.exitValue() != 0) {
492                 // Command problem, throw exception
493                 throw new IOException("Command line returned OS error code '" + proc.exitValue() + "' for command " + Arrays.asList(cmdAttribs));
494             }
495             if (lines.isEmpty()) {
496                 // Unknown problem, throw exception
497                 throw new IOException("Command line did not return any info for command " + Arrays.asList(cmdAttribs));
498             }
499 
500             return lines;
501 
502         } catch (final InterruptedException ex) {
503             throw new IOException("Command line threw an InterruptedException for command " + Arrays.asList(cmdAttribs) + " timeout=" + timeout, ex);
504         } finally {
505             if (proc != null) {
506                 proc.destroy();
507             }
508         }
509     }
510 
511 }
512