/*
**  Reports free kilobytes (not disk blocks) or free inodes.
**
**  Written by Ian Dickinson <idickins@fore.com>
**  Wed Jul 26 10:11:38 BST 1995 (My birthday - 27 today!)
**
**  Various bug fixes, code and documentation improvements since then
**  in 1998-2002, 2004-2006, 2014, 2015, 2017, 2021, 2024.
**
**  inndf is a replacement for 'df | awk' in innwatch.ctl and for reporting
**  free space in other INN scripts.  It doesn't sync, it forks less, and
**  it's generally less complicated.
**
**  Usage: inndf [-i] <directory> [<directory> ...]
**         inndf -n
**         inndf -o
**
**  Compile with -lserver (ie. /usr/lib/libserver.a) if you run Sun's Online
**  DiskSuite under SunOS 4.x.  The wrapper functions there make the system
**  call transparent; they copy the f_spare values to the correct spots, so
**  f_blocks, f_bfree, f_bavail can exceed 2GB.
**
**  Compile with -DHAVE_STATVFS for these systems:
**          System V Release 4.x
**          Solaris 2.x
**          HP-UX 10.x
**          OSF1
**
**  Compile with -DHAVE_STATFS for these systems:
**          SunOS 4.x/Solaris 1.x
**          HP-UX 9.x
**          Linux
**          NeXTstep 3.x
**
**  (Or even better, let autoconf take care of it.)
**
**  Thanks to these folks for bug fixes and porting information:
**          Mahesh Ramachandran <rr@eel.ufl.edu>
**          Chuck Swiger <chuck@its.com>
**          Sang-yong Suh <sysuh@kigam.re.kr>
**          Swa Frantzen <Swa.Frantzen@Belgium.EU.net>
**          Brad Dickey <bdickey@haverford.edu>
**          Taso N. Devetzis <devetzis@snet.net>
**          Wei-Yeh Lee <weiyeh@columbia.edu>
**          Jeff Garzik <jeff.garzik@spinne.com>
*/

#include "portable/system.h"

#include "inn/innconf.h"
#include "inn/libinn.h"
#include "inn/messages.h"
#include "inn/overview.h"
#include "inn/paths.h"
#include "inn/qio.h"

/* The portability mess.  Hide everything in macros so that the actual code
   is relatively clean.  SysV uses statvfs, BSD uses statfs, and ULTRIX is
   just weird (and isn't worth checking for in configure).

   df_declare declares a variable of the appropriate type to pass to df_stat
   along with a path; df_stat will return true on success, false on failure.
   df_avail gives the number of free blocks, the size of those blocks given
   in df_scale (which handles SysV's weird fragment vs. preferred block size
   thing).  df_favail returns the free inodes. */
#if HAVE_STATVFS
#    include <sys/statvfs.h>
#    define df_stat(p, s) (statvfs((p), (s)) == 0)
#    define df_declare(s) struct statvfs s
#    define df_total(s)   ((s).f_blocks)
#    define df_avail(s)   ((s).f_bavail)
#    define df_scale(s)   ((s).f_frsize == 0 ? (s).f_bsize : (s).f_frsize)
#    define df_files(s)   ((s).f_files)
#    define df_favail(s)  ((s).f_favail)
#elif HAVE_STATFS
#    if HAVE_SYS_VFS_H
#        include <sys/vfs.h>
#    endif
#    if HAVE_SYS_PARAM_H
#        include <sys/param.h>
#    endif
#    if HAVE_SYS_MOUNT_H
#        include <sys/mount.h>
#    endif
#    ifdef __ultrix__
#        define df_stat(p, s) (statfs((p), (s)) >= 1)
#        define df_declare(s) struct fs_data s
#        define df_total(s)   ((s).fd_btot)
#        define df_avail(s)   ((s).fd_bfreen)
#        define df_scale(s)   1024
#        define df_files(s)   ((s).fd_gtot)
#        define df_favail(s)  ((s).fd_gfree)
#    else
#        define df_stat(p, s) (statfs((p), (s)) == 0)
#        define df_declare(s) struct statfs s
#        define df_total(s)   ((s).f_blocks)
#        define df_avail(s)   ((s).f_bavail)
#        define df_scale(s)   ((s).f_bsize)
#        define df_files(s)   ((s).f_files)
#        define df_favail(s)  ((s).f_ffree)
#    endif
#else
#    error "Platform not supported.  Neither statvfs nor statfs available."
#endif

/* Pick the longest available integer type. */
#if HAVE_LONG_LONG_INT
typedef unsigned long long long_int_type;
#    define LLFORMAT "llu"
#else
typedef unsigned long long_int_type;
#    define LLFORMAT "lu"
#endif

static const char usage[] = "\
Usage: inndf [-i] [-f filename] [-F] <directory> [<directory> ...]\n\
       inndf -n\n\
       inndf -o\n\
\n\
The first form gives the free space in kilobytes (or the count of free\n\
inodes if -i is given) in the file systems given by the arguments.  If\n\
-f is given, the corresponding file should be a list of directories to\n\
check in addition to the arguments.  -F uses <pathetc>/filesystems as the\n\
file and is otherwise the same.\n\
\n\
The second form gives the total count of overview records stored.  The\n\
third form gives the percentage space allocated to overview that's been\n\
used (if the overview method used supports this query).";

/*
**  Given a path, a flag saying whether to look at inodes instead of free
**  disk space, and a flag saying whether to format in columns, print out
**  the amount of free space or inodes on that file system.  Returns the
**  percentage free, which may be printed out by the caller.
*/
static void
printspace(const char *path, bool inode, bool fancy)
{
    df_declare(info);
    long_int_type amount;
    double percent;

    if (df_stat(path, &info)) {
        if (inode) {
            amount = df_favail(info);

            /* This value is compared using the shell by innwatch, and some
               shells can't cope with anything larger than the maximum value
               of a signed long.  ReiserFS returns 2^32 - 1, however, since it
               has no concept of inodes.  So cap the returned value at the max
               value of a 32-bit signed long. */
            if (amount > (1UL << 31) - 1)
                amount = (1UL << 31) - 1;

            /* 2.6 kernels show 0 available and used inodes, instead. */
            if (amount == 0 && df_files(info) == 0)
                amount = (1UL << 31) - 1;
        } else {
            /* Do the multiplication in floating point to try to retain
               accuracy if the free space in bytes would overflow a 32-bit
               unsigned long.  This should be safe for file systems smaller
               than 4TB (that's why we use long long, if we have it).

               Be very careful about the order of casts here; it's too
               easy to cast back into an unsigned long a value that
               overflows, and one then gets silently wrong results. */
            amount = (long_int_type) (((double) df_avail(info)
                                       * (double) df_scale(info))
                                      / 1024.0);
        }
    } else {
        /* On error, free space is zero. */
        amount = 0;
    }
    printf(fancy ? "%10" LLFORMAT : "%" LLFORMAT, amount);
    if (fancy) {
        printf(inode ? " inodes available " : " Kbytes available ");
        if (inode)
            percent =
                100 * ((double) df_favail(info) / (double) df_files(info));
        else
            percent =
                100 * ((double) df_avail(info) / (double) df_total(info));
        if (percent < 9.95)
            printf("  (%3.1f%%)", percent);
        else if (percent < 99.95)
            printf(" (%4.1f%%)", percent);
        else
            printf("(%5.1f%%)", percent);
    }
}

static void
printspace_formatted(const char *path, bool inode)
{
    printf("%-40s ", path);
    printspace(path, inode, true);
    printf("\n");
}

static char *
readline(QIOSTATE *qp)
{
    char *line, *p;

    for (line = QIOread(qp); line != NULL; line = QIOread(qp)) {
        p = strchr(line, '#');
        if (p != NULL)
            *p = '\0';
        for (; *line == ' ' || *line == '\t'; line++)
            ;
        if (*line != '\0') {
            for (p = line; *p != '\0' && *p != ' ' && *p != '\t'; p++)
                ;
            *p = '\0';
            return line;
        }
    }
    return NULL;
}

int
main(int argc, char *argv[])
{
    int option, i;
    unsigned long total;
    QIOSTATE *qp;
    char *active, *group, *line, *p;
    struct overview *overview = NULL;
    struct overview_group stats;
    float used;
    char *file = NULL;
    bool inode = false;
    bool ovcount = false;
    bool ovused = false;
    bool use_filesystems = false;

    while ((option = getopt(argc, argv, "hinof:F")) != EOF) {
        switch (option) {
        default:
            die(usage);
        case 'h':
            printf("%s\n", usage);
            exit(0);
        case 'i':
            inode = true;
            break;
        case 'n':
            ovcount = true;
            break;
        case 'o':
            ovused = true;
            break;
        case 'f':
            if (file != NULL)
                die("inndf: Only one of -f or -F may be given");
            file = xstrdup(optarg);
            break;
        case 'F':
            if (file != NULL)
                die("inndf: Only one of -f or -F may be given");
            if (!innconf_read(NULL))
                exit(1);
            file = concatpath(innconf->pathetc, INN_PATH_FILESYSTEMS);
            use_filesystems = true;
            break;
        }
    }
    argc -= optind;
    argv += optind;

    if (argc == 0 && !ovused && !ovcount && file == NULL)
        die(usage);

    /* Set the program name now rather than earlier so that it doesn't get
       prepended to usage messages. */
    message_program_name = "inndf";

    /* If directories were specified, get statistics about them.  If only
       one was given, just print out the number without the path or any
       explanatory text; this mode is used by e.g. innwatch.  Otherwise,
       format things nicely. */
    if (argc == 1 && !ovused && !ovcount && file == NULL) {
        printspace(argv[0], inode, false);
        printf("\n");
    } else {
        for (i = 0; i < argc; i++)
            printspace_formatted(argv[i], inode);
        if (file != NULL) {
            qp = QIOopen(file);
            if (qp == NULL) {
                if (!use_filesystems)
                    sysdie("can't open %s", file);
            } else {
                line = readline(qp);
                while (line != NULL) {
                    printspace_formatted(line, inode);
                    line = readline(qp);
                }
                QIOclose(qp);
            }
            free(file);
        }
    }

    /* If we're going to be getting information from overview, do the icky
       initialization stuff. */
    if (ovused || ovcount) {
        if (!use_filesystems)
            if (!innconf_read(NULL))
                exit(1);
        overview = overview_open(OV_READ);
        if (overview == NULL)
            die("can't open overview");
    }

    /* For the count, we have to troll through the active file and query the
       overview backend for each group. */
    if (ovcount) {
        active = concatpath(innconf->pathdb, INN_PATH_ACTIVE);
        qp = QIOopen(active);
        if (qp == NULL)
            sysdie("can't open %s", active);

        total = 0;
        group = QIOread(qp);
        while (group != NULL) {
            p = strchr(group, ' ');
            if (p != NULL)
                *p = '\0';
            if (overview_group(overview, group, &stats))
                total += stats.count;
            group = QIOread(qp);
        }
        QIOclose(qp);
        printf("%lu overview records stored\n", total);
    }

    /* Percentage used is simpler, but only some overview methods understand
       that query. */
    if (ovused) {
        used = overview_free_space(overview);
        if (used < 0)
            printf("Space used is meaningless for the %s method\n",
                   innconf->ovmethod);
        else
            printf("%.2f%% overview space used\n", (double) used);
    }
    if (overview != NULL)
        overview_close(overview);
    exit(0);
}
