    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 */
    017package org.apache.commons.io;
    018
    019import java.io.BufferedReader;
    020import java.io.IOException;
    021import java.io.InputStream;
    022import java.io.InputStreamReader;
    023import java.io.OutputStream;
    024import java.nio.charset.Charset;
    025import java.nio.file.InvalidPathException;
    026import java.nio.file.Path;
    027import java.nio.file.Paths;
    028import java.time.Duration;
    029import java.util.Arrays;
    030import java.util.List;
    031import java.util.Locale;
    032import java.util.Objects;
    033import java.util.StringTokenizer;
    034import java.util.stream.Collectors;
    035
    036/**
    037 * General File System utilities.
    038 * <p>
    039 * This class provides static utility methods for general file system functions not provided via the JDK {@link java.io.File File} class.
    040 * <p>
    041 * The current functions provided are:
    042 * <ul>
    043 * <li>Get the free space on a drive
    044 * </ul>
    045 *
    046 * @since 1.1
    047 * @deprecated As of 2.6 deprecated without replacement. Use equivalent methods in {@link java.nio.file.FileStore} instead, e.g.
    048 *             {@code Files.getFileStore(Paths.get("/home")).getUsableSpace()} or iterate over {@code FileSystems.getDefault().getFileStores()}
    049 */
    050@Deprecated
    051public class FileSystemUtils {
    052
    053    /**
    054     * Singleton instance, used mainly for testing.
    055     */
    056    private static final FileSystemUtils INSTANCE = new FileSystemUtils();
    057
    058    /**
    059     * Operating system state flag for error.
    060     */
    061    private static final int INIT_PROBLEM = -1;
    062
    063    /**
    064     * Operating system state flag for neither UNIX nor Windows.
    065     */
    066    private static final int OTHER = 0;
    067
    068    /**
    069     * Operating system state flag for Windows.
    070     */
    071    private static final int WINDOWS = 1;
    072
    073    /**
    074     * Operating system state flag for Unix.
    075     */
    076    private static final int UNIX = 2;
    077
    078    /**
    079     * Operating system state flag for POSIX flavor Unix.
    080     */
    081    private static final int POSIX_UNIX = 3;
    082
    083    /**
    084     * The operating system flag.
    085     */
    086    private static final int OS;
    087
    088    /**
    089     * The path to {@code df}.
    090     */
    091    private static final String DF;
    092
    093    static {
    094        int os = OTHER;
    095        String dfPath = "df";
    096        try {
    097            String osName = System.getProperty("os.name");
    098            if (osName == null) {
    099                throw new IOException("os.name not found");
    100            }
    101            osName = osName.toLowerCase(Locale.ENGLISH);
    102            // match
    103            if (osName.contains("windows")) {
    104                os = WINDOWS;
    105            } else if (osName.contains("linux") || osName.contains("mpe/ix") || osName.contains("freebsd") || osName.contains("openbsd")
    106                    || osName.contains("irix") || osName.contains("digital unix") || osName.contains("unix") || osName.contains("mac os x")) {
    107                os = UNIX;
    108            } else if (osName.contains("sun os") || osName.contains("sunos") || osName.contains("solaris")) {
    109                os = POSIX_UNIX;
    110                dfPath = "/usr/xpg4/bin/df";
    111            } else if (osName.contains("hp-ux") || osName.contains("aix")) {
    112                os = POSIX_UNIX;
    113            }
    114
    115        } catch (final Exception ex) {
    116            os = INIT_PROBLEM;
    117        }
    118        OS = os;
    119        DF = dfPath;
    120    }
    121
    122    /**
    123     * Returns the free space on a drive or volume by invoking the command line. This method does not normalize the result, and typically returns bytes on
    124     * Windows, 512 byte units on OS X and kilobytes on Unix. As this is not very useful, this method is deprecated in favor of {@link #freeSpaceKb(String)}
    125     * which returns a result in kilobytes.
    126     * <p>
    127     * Note that some OS's are NOT currently supported, including OS/390, OpenVMS.
    128     *
    129     * <pre>
    130     * FileSystemUtils.freeSpace("C:"); // Windows
    131     * FileSystemUtils.freeSpace("/volume"); // *nix
    132     * </pre>
    133     *
    134     * The free space is calculated via the command line. It uses 'dir /-c' on Windows and 'df' on *nix.
    135     *
    136     * @param path the path to get free space for, not null, not empty on UNIX
    137     * @return the amount of free drive space on the drive or volume
    138     * @throws IllegalArgumentException if the path is invalid
    139     * @throws IllegalStateException    if an error occurred in initialization
    140     * @throws IOException              if an error occurs when finding the free space
    141     * @since 1.1, enhanced OS support in 1.2 and 1.3
    142     * @deprecated Use freeSpaceKb(String) Deprecated from 1.3, may be removed in 2.0
    143     */
    144    @Deprecated
    145    public static long freeSpace(final String path) throws IOException {
    146        return INSTANCE.freeSpaceOS(path, OS, false, Duration.ofMillis(-1));
    147    }
    148
    149    /**
    150     * Returns the free space for the working directory in kibibytes (1024 bytes) by invoking the command line.
    151     * <p>
    152     * Identical to:
    153     *
    154     * <pre>
    155     * freeSpaceKb(FileUtils.current().getAbsolutePath())
    156     * </pre>
    157     *
    158     * @return the amount of free drive space on the drive or volume in kilobytes
    159     * @throws IllegalStateException if an error occurred in initialization
    160     * @throws IOException           if an error occurs when finding the free space
    161     * @since 2.0
    162     * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
    163     */
    164    @Deprecated
    165    public static long freeSpaceKb() throws IOException {
    166        return freeSpaceKb(-1);
    167    }
    168
    169    /**
    170     * Returns the free space for the working directory in kibibytes (1024 bytes) by invoking the command line.
    171     * <p>
    172     * Identical to:
    173     *
    174     * <pre>
    175     * freeSpaceKb(FileUtils.current().getAbsolutePath())
    176     * </pre>
    177     *
    178     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
    179     * @return the amount of free drive space on the drive or volume in kilobytes
    180     * @throws IllegalStateException if an error occurred in initialization
    181     * @throws IOException           if an error occurs when finding the free space
    182     * @since 2.0
    183     * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
    184     */
    185    @Deprecated
    186    public static long freeSpaceKb(final long timeout) throws IOException {
    187        return freeSpaceKb(FileUtils.current().getAbsolutePath(), timeout);
    188    }
    189
    190    /**
    191     * Returns the free space on a drive or volume in kibibytes (1024 bytes) by invoking the command line.
    192     *
    193     * <pre>
    194     * FileSystemUtils.freeSpaceKb("C:"); // Windows
    195     * FileSystemUtils.freeSpaceKb("/volume"); // *nix
    196     * </pre>
    197     *
    198     * The free space is calculated via the command line. It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
    199     * <p>
    200     * In order to work, you must be running Windows, or have an implementation of UNIX df that supports GNU format when passed -k (or -kP). If you are going to
    201     * rely on this code, please check that it works on your OS by running some simple tests to compare the command line with the output from this class. If
    202     * your operating system isn't supported, please raise a JIRA call detailing the exact result from df -k and as much other detail as possible, thanks.
    203     *
    204     * @param path the path to get free space for, not null, not empty on UNIX
    205     * @return the amount of free drive space on the drive or volume in kilobytes
    206     * @throws IllegalArgumentException if the path is invalid
    207     * @throws IllegalStateException    if an error occurred in initialization
    208     * @throws IOException              if an error occurs when finding the free space
    209     * @since 1.2, enhanced OS support in 1.3
    210     * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
    211     */
    212    @Deprecated
    213    public static long freeSpaceKb(final String path) throws IOException {
    214        return freeSpaceKb(path, -1);
    215    }
    216
    217    /**
    218     * Returns the free space on a drive or volume in kibibytes (1024 bytes) by invoking the command line.
    219     *
    220     * <pre>
    221     * FileSystemUtils.freeSpaceKb("C:"); // Windows
    222     * FileSystemUtils.freeSpaceKb("/volume"); // *nix
    223     * </pre>
    224     *
    225     * The free space is calculated via the command line. It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
    226     * <p>
    227     * In order to work, you must be running Windows, or have an implementation of UNIX df that supports GNU format when passed -k (or -kP). If you are going to
    228     * rely on this code, please check that it works on your OS by running some simple tests to compare the command line with the output from this class. If
    229     * your operating system isn't supported, please raise a JIRA call detailing the exact result from df -k and as much other detail as possible, thanks.
    230     *
    231     * @param path    the path to get free space for, not null, not empty on UNIX
    232     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
    233     * @return the amount of free drive space on the drive or volume in kilobytes
    234     * @throws IllegalArgumentException if the path is invalid
    235     * @throws IllegalStateException    if an error occurred in initialization
    236     * @throws IOException              if an error occurs when finding the free space
    237     * @since 2.0
    238     * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
    239     */
    240    @Deprecated
    241    public static long freeSpaceKb(final String path, final long timeout) throws IOException {
    242        return INSTANCE.freeSpaceOS(path, OS, true, Duration.ofMillis(timeout));
    243    }
    244
    245    /**
    246     * Instances should NOT be constructed in standard programming.
    247     *
    248     * @deprecated TODO Make private in 3.0.
    249     */
    250    @Deprecated
    251    public FileSystemUtils() {
    252        // empty
    253    }
    254
    255    /**
    256     * Checks that a path string is valid through NIO's {@link Paths#get(String, String...)}.
    257     *
    258     * @param pathStr string.
    259     * @param allowEmpty allows empty paths.
    260     * @return A checked normalized Path.
    261     * @throws InvalidPathException if the path string cannot be converted to a {@code Path}
    262     */
    263    private Path checkPath(final String pathStr, final boolean allowEmpty) {
    264        Objects.requireNonNull(pathStr, "pathStr");
    265        if (!allowEmpty && pathStr.isEmpty()) {
    266            throw new IllegalArgumentException("Path must not be empty");
    267        }
    268        final Path normPath;
    269        final String trimPathStr = pathStr.trim();
    270        if (trimPathStr.isEmpty() || trimPathStr.charAt(0) != '"') {
    271            // Paths.get throws InvalidPathException if the path is bad before we pass it to a shell.
    272            normPath = Paths.get(trimPathStr).normalize();
    273        } else {
    274            // Paths.get throws InvalidPathException if the path is bad before we pass it to a shell.
    275            normPath = Paths.get(trimPathStr.substring(1, trimPathStr.length() - 1)).normalize();
    276        }
    277        return normPath;
    278    }
    279
    280    /**
    281     * Returns the free space on a drive or volume in a cross-platform manner. Note that some OS's are NOT currently supported, including OS/390.
    282     *
    283     * <pre>
    284     * FileSystemUtils.freeSpace("C:"); // Windows
    285     * FileSystemUtils.freeSpace("/volume"); // *nix
    286     * </pre>
    287     *
    288     * The free space is calculated via the command line. It uses 'dir /-c' on Windows and 'df' on *nix.
    289     *
    290     * @param pathStr the path to get free space for, not null, not empty on UNIX
    291     * @param os      the operating system code
    292     * @param kb      whether to normalize to kilobytes
    293     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
    294     * @return the amount of free drive space on the drive or volume
    295     * @throws IllegalArgumentException if the path is invalid
    296     * @throws IllegalStateException    if an error occurred in initialization
    297     * @throws IOException              if an error occurs when finding the free space
    298     */
    299    long freeSpaceOS(final String pathStr, final int os, final boolean kb, final Duration timeout) throws IOException {
    300        Objects.requireNonNull(pathStr, "path");
    301        switch (os) {
    302        case WINDOWS:
    303            return kb ? freeSpaceWindows(pathStr, timeout) / FileUtils.ONE_KB : freeSpaceWindows(pathStr, timeout);
    304        case UNIX:
    305            return freeSpaceUnix(pathStr, kb, false, timeout);
    306        case POSIX_UNIX:
    307            return freeSpaceUnix(pathStr, kb, true, timeout);
    308        case OTHER:
    309            throw new IllegalStateException("Unsupported operating system");
    310        default:
    311            throw new IllegalStateException("Exception caught when determining operating system");
    312        }
    313    }
    314
    315    /**
    316     * Finds free space on the *nix platform using the 'df' command.
    317     *
    318     * @param path    the path to get free space for
    319     * @param kb      whether to normalize to kilobytes
    320     * @param posix   whether to use the POSIX standard format flag
    321     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
    322     * @return the amount of free drive space on the volume
    323     * @throws IOException If an I/O error occurs
    324     */
    325    long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout) throws IOException {
    326        final String pathStr = checkPath(path, false).toString();
    327        // build and run the 'dir' command
    328        String flags = "-";
    329        if (kb) {
    330            flags += "k";
    331        }
    332        if (posix) {
    333            flags += "P";
    334        }
    335        final String[] cmdAttribs = flags.length() > 1 ? new String[] { DF, flags, pathStr } : new String[] { DF, pathStr };
    336
    337        // perform the command, asking for up to 3 lines (header, interesting, overflow)
    338        final List<String> lines = performCommand(cmdAttribs, 3, timeout);
    339        if (lines.size() < 2) {
    340            // unknown problem, throw exception
    341            throw new IOException("Command line '" + DF + "' did not return info as expected for path '" + pathStr + "'- response was " + lines);
    342        }
    343        final String line2 = lines.get(1); // the line we're interested in
    344
    345        // Now, we tokenize the string. The fourth element is what we want.
    346        StringTokenizer tok = new StringTokenizer(line2, " ");
    347        if (tok.countTokens() < 4) {
    348            // could be long Filesystem, thus data on third line
    349            if (tok.countTokens() != 1 || lines.size() < 3) {
    350                throw new IOException("Command line '" + DF + "' did not return data as expected for path '" + pathStr + "'- check path is valid");
    351            }
    352            final String line3 = lines.get(2); // the line may be interested in
    353            tok = new StringTokenizer(line3, " ");
    354        } else {
    355            tok.nextToken(); // Ignore Filesystem
    356        }
    357        tok.nextToken(); // Ignore 1K-blocks
    358        tok.nextToken(); // Ignore Used
    359        final String freeSpace = tok.nextToken();
    360        return parseBytes(freeSpace, path);
    361    }
    362
    363    /**
    364     * Finds free space on the Windows platform using the 'dir' command.
    365     *
    366     * @param pathStr    the path to get free space for, including the colon
    367     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
    368     * @return the amount of free drive space on the drive
    369     * @throws IOException If an I/O error occurs
    370     */
    371    long freeSpaceWindows(final String pathStr, final Duration timeout) throws IOException {
    372        final Path path = checkPath(pathStr, true);
    373        // build and run the 'dir' command
    374        // read in the output of the command to an ArrayList
    375        final List<String> lines = performCommand(new String[] { "cmd.exe", "/C", "dir /a /-c \"" + path + "\"" }, Integer.MAX_VALUE, timeout);
    376
    377        // now iterate over the lines we just read and find the LAST
    378        // non-empty line (the free space bytes should be in the last element
    379        // of the ArrayList anyway, but this will ensure it works even if it's
    380        // not, still assuming it is on the last non-blank line)
    381        for (int i = lines.size() - 1; i >= 0; i--) {
    382            final String line = lines.get(i);
    383            if (!line.isEmpty()) {
    384                return parseDir(line, pathStr);
    385            }
    386        }
    387        // all lines are blank
    388        throw new IOException("Command 'dir' did not return any info for path '" + path + "'");
    389    }
    390
    391    /**
    392     * Opens the process to the operating system.
    393     * <p>
    394     * Package-private for tests.
    395     * </p>
    396     * @param cmdArray the command line parameters
    397     * @return the process
    398     * @throws IOException If an I/O error occurs
    399     */
    400    Process openProcess(final String[] cmdArray) throws IOException {
    401        return Runtime.getRuntime().exec(cmdArray);
    402    }
    403
    404    /**
    405     * Parses the bytes from a string.
    406     *
    407     * @param freeSpace the free space string
    408     * @param path      the path
    409     * @return the number of bytes
    410     * @throws IOException If an I/O error occurs
    411     */
    412    private long parseBytes(final String freeSpace, final String path) throws IOException {
    413        try {
    414            final long bytes = Long.parseLong(freeSpace);
    415            if (bytes < 0) {
    416                throw new IOException("Command line '" + DF + "' did not find free space in response for path '" + path + "'- check path is valid");
    417            }
    418            return bytes;
    419
    420        } catch (final NumberFormatException ex) {
    421            throw new IOException("Command line '" + DF + "' did not return numeric data as expected for path '" + path + "'- check path is valid", ex);
    422        }
    423    }
    424
    425    /**
    426     * Parses the Windows dir response last line.
    427     *
    428     * @param line the line to parse
    429     * @param path the path that was sent
    430     * @return the number of bytes
    431     * @throws IOException If an I/O error occurs
    432     */
    433    private long parseDir(final String line, final String path) throws IOException {
    434        // read from the end of the line to find the last numeric
    435        // character on the line, then continue until we find the first
    436        // non-numeric character, and everything between that and the last
    437        // numeric character inclusive is our free space bytes count
    438        int bytesStart = 0;
    439        int bytesEnd = 0;
    440        int j = line.length() - 1;
    441        innerLoop1: while (j >= 0) {
    442            final char c = line.charAt(j);
    443            if (Character.isDigit(c)) {
    444                // found the last numeric character, this is the end of
    445                // the free space bytes count
    446                bytesEnd = j + 1;
    447                break innerLoop1;
    448            }
    449            j--;
    450        }
    451        innerLoop2: while (j >= 0) {
    452            final char c = line.charAt(j);
    453            if (!Character.isDigit(c) && c != ',' && c != '.') {
    454                // found the next non-numeric character, this is the
    455                // beginning of the free space bytes count
    456                bytesStart = j + 1;
    457                break innerLoop2;
    458            }
    459            j--;
    460        }
    461        if (j < 0) {
    462            throw new IOException("Command line 'dir /-c' did not return valid info for path '" + path + "'");
    463        }
    464
    465        // remove commas and dots in the bytes count
    466        final StringBuilder buf = new StringBuilder(line.substring(bytesStart, bytesEnd));
    467        for (int k = 0; k < buf.length(); k++) {
    468            if (buf.charAt(k) == ',' || buf.charAt(k) == '.') {
    469                buf.deleteCharAt(k--);
    470            }
    471        }
    472        return parseBytes(buf.toString(), path);
    473    }
    474
    475    /**
    476     * Performs an OS command.
    477     *
    478     * @param cmdArray the command line parameters
    479     * @param max        The maximum limit for the lines returned
    480     * @param timeout    The timeout amount in milliseconds or no timeout if the value is zero or less
    481     * @return the lines returned by the command, converted to lower-case
    482     * @throws IOException if an error occurs
    483     */
    484    private List<String> performCommand(final String[] cmdArray, final int max, final Duration timeout) throws IOException {
    485        //
    486        // This method does what it can to avoid the 'Too many open files' error
    487        // based on trial and error and these links:
    488        // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4784692
    489        // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4801027
    490        // However, it's still not perfect as the JDK support is so poor.
    491        // (See commons-exec or Ant for a better multithreaded multi-OS solution.)
    492        //
    493        final Process proc = openProcess(cmdArray);
    494        final Thread monitor = ThreadMonitor.start(timeout);
    495        try (InputStream in = proc.getInputStream();
    496                OutputStream out = proc.getOutputStream();
    497                // default Charset is most likely appropriate here
    498                InputStream err = proc.getErrorStream();
    499                // If in is null here, InputStreamReader throws NullPointerException
    500                BufferedReader inr = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
    501
    502            final List<String> lines = inr.lines().limit(max).map(line -> line.toLowerCase(Locale.getDefault()).trim()).collect(Collectors.toList());
    503            proc.waitFor();
    504            ThreadMonitor.stop(monitor);
    505
    506            if (proc.exitValue() != 0) {
    507                // Command problem, throw exception
    508                throw new IOException("Command line returned OS error code '" + proc.exitValue() + "' for command " + Arrays.asList(cmdArray));
    509            }
    510            if (lines.isEmpty()) {
    511                // Unknown problem, throw exception
    512                throw new IOException("Command line did not return any info for command " + Arrays.asList(cmdArray));
    513            }
    514
    515            return lines;
    516
    517        } catch (final InterruptedException ex) {
    518            throw new IOException("Command line threw an InterruptedException for command " + Arrays.asList(cmdArray) + " timeout=" + timeout, ex);
    519        } finally {
    520            if (proc != null) {
    521                proc.destroy();
    522            }
    523        }
    524    }
    525
    526}