/* backup.c
 *
 * Copyright (C) 1999-2002 by Jason Day
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "config.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <utime.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <gdbm.h>
#include <dirent.h>
#include <libgen.h>
#include <errno.h>

#include "backup.h"
#include "libplugin.h"
#include "bprefs.h"


static const char RCSID[] = "$Id: backup.c,v 1.1.1.1 2003/02/04 18:01:30 rousseau Exp $";


/* Local static functions */
static int archive_dir_select (const struct dirent *entry);
static time_t get_last_backup_time (struct tm *timep);



/*
 * This is called by plugin_sync to see if we need to skip the sync based
 * on the user's preferences. We just check the last sync time, compare it
 * with the current time, then see if the required amount of time has
 * passed.
 *
 * Returns TRUE if the sync should be skipped,
 *    ie TRUE == do not sync, FALSE == perform sync
 */
gboolean skip_backup() {
    time_t ltime;
    struct tm *timep;
    long ivalue;
    gboolean rval = FALSE;   /* DON'T skip by default */


    /*
     * Note that, despite what the man pages would seem to indicate, the
     * localtime function returns a pointer to a single globally allocated
     * structure which is overwritten with each call.
     */
    time (&ltime);
    timep = localtime (&ltime);
    jp_logf (JP_LOG_DEBUG,
             "Backup::skip_backup() - now = %d/%d/%d %d:%d:%d\n",
             timep->tm_mon + 1, timep->tm_mday, timep->tm_year + 1900,
             timep->tm_hour, timep->tm_min, timep->tm_sec);

    get_last_backup_time (timep);
    jp_logf (JP_LOG_DEBUG,
             "Backup::skip_backup() - last backup = %d/%d/%d %d:%d:%d\n",
             timep->tm_mon + 1, timep->tm_mday, timep->tm_year + 1900,
             timep->tm_hour, timep->tm_min, timep->tm_sec);

    /* Get the backup-when pref, and decide if we need to sync. */
    backup_get_pref (BPREF_BACKUP_WHEN, &ivalue, NULL);
    switch (ivalue) {
        case EVERY_SYNC:
            rval = FALSE;
            break;

        case DAILY:
            timep->tm_mday++;
            if (mktime (timep) > ltime) {
                rval = TRUE;
            }
            jp_logf (JP_LOG_DEBUG,
                     "Backup::skip_backup() - adjusted last backup = %d/%d/%d %d:%d:%d\n",
                     timep->tm_mon + 1, timep->tm_mday, timep->tm_year + 1900,
                     timep->tm_hour, timep->tm_min, timep->tm_sec);
            jp_logf (JP_LOG_DEBUG, "ltime = %ld, mktime = %ld\n", ltime, mktime (timep));
            break;

        case WEEKLY:
            timep->tm_mday += 7;
            if (mktime (timep) > ltime) {
                rval = TRUE;
            }
            break;

        case MONTHLY:
            timep->tm_mon++;
            if (mktime (timep) > ltime) {
                rval = TRUE;
            }
            break;

        default:
            jp_logf (JP_LOG_WARN,
                         "Unrecognized pref value for backup_when: %d\n",
                         ivalue);
    }

    return rval;
}

/*
 * Used by expire_archives() to list archive directories.
 */
static int archive_dir_select (const struct dirent *entry) {
    int i, j, k;
    int x, y, z;

    return sscanf (entry->d_name,
                   "Archive_%4d-%2d-%2d@%2d:%2d:%2d",
                   &i, &j, &k, &x, &y, &z);
}

/*
 * Expires the given archive, ie, deletes the directory.
 */
int expire_archive (char *dir) {
    FILE *fp;
    char full_name[256];
    char line[256];
    char *pc;

    jp_logf (JP_LOG_GUI, "Expiring %s\n", dir);

    get_archive_file_name (dir, MANIFEST, full_name, 255);
    fp = fopen (full_name, "r");
    if (!fp) {
        jp_logf (JP_LOG_WARN,
                 "Can't open manifest file %s.\n"
                 "Please delete archive directory %s by hand.\n",
                 full_name, dir);
        return -1;
    }

    while (!feof (fp)) {
        if (fgets (line, 256, fp)) {

            /* chop off the newline */
            if ((pc = (char *)index (line, '\n'))) {
                pc[0] = '\0';
            }

            get_archive_file_name (dir, line, full_name, 255);
            if (unlink (full_name) < 0) {
                perror ("unlink");
                jp_logf (JP_LOG_WARN,
                         "Can't delete archive file %s.\n"
                         "Please delete archive directory %s by hand.\n",
                         full_name, dir);
            }
        }
    }

    fclose (fp);
    get_archive_file_name (dir, MANIFEST, full_name, 255);
    unlink (full_name);

    if (rmdir (dir) < 0) {
        perror ("rmdir");
        jp_logf (JP_LOG_WARN,
                 "Can't remove archive directory %s.\n"
                 "Please delete by hand.\n",
                 dir);
    }

    return 0;
}

/*
 * Checks to see how many archive directories there currently are, and
 * if there are more than set in BPREF_NUM_ARCHIVES, expires the oldest
 * director(y|ies).
 */
int expire_archives() {
    struct dirent **namelist;
    char temp_str[256];
    int n, i, j;
    long ivalue;
    char backup_dir[260];


    jp_get_home_file_name(BACKUP_DIR_NAME, backup_dir, 256);

    jp_logf (JP_LOG_GUI, "Expiring old archives...\n");

    n = scandir (backup_dir, &namelist, archive_dir_select, alphasort);
    if (n < 0) {
        perror("scandir");
        jp_logf (JP_LOG_WARN,
                 "Unable to scan backup directory %s.\n"
                 "Hence, unable to expire archives\n",
                 backup_dir);
        return -1;
    }

    backup_get_pref (BPREF_NUM_ARCHIVES, &ivalue, NULL);
    jp_logf (JP_LOG_DEBUG, "Backup: expire_archives - pref: %d, %d archives exist.\n", ivalue, n);
    for (i = 0, j = n; j > ivalue; i++, j--) {
        get_backup_file_name (namelist[i]->d_name, temp_str, 255);
        expire_archive (temp_str);
        free (namelist[i]);
    }

    /* free the remaining items in namelist */
    while (i < n) {
        free (namelist[i++]);
    }

    if (namelist) {
        free (namelist);
    }
    return 0;
}

/*
 * Checks for the existence of the backup directory, and creates it if
 * necessary.
 */
int check_backup_dir() {
    struct stat statb;
    char backup_dir[260];
    char test_file[260];
    FILE *out;

    jp_get_home_file_name(BACKUP_DIR_NAME, backup_dir, 256);

    if (stat(backup_dir, &statb)) {
        /*directory isn't there, create it */
        if (mkdir(backup_dir, 0777)) {
            /*Can't create directory */
            jp_logf(JP_LOG_WARN, "Can't create directory %s\n", backup_dir);
            return 1;
        }
        if (stat(backup_dir, &statb)) {
            jp_logf(JP_LOG_WARN, "Can't create directory %s\n", backup_dir);
            return 1;
        }
    }

    /* Is it a directory? */
    if (!S_ISDIR(statb.st_mode)) {
        jp_logf(JP_LOG_WARN, "%s doesn't appear to be a directory.\n"
                    "I need it to be.\n", backup_dir);
        return 1;
    }

    /* Can we write in it? */
    get_backup_file_name ("test", test_file, 256);
    jp_logf (JP_LOG_DEBUG, "Trying to open %s for write\n", test_file);
    out = fopen(test_file, "w+");
    if (!out) {
        jp_logf(JP_LOG_WARN, "I can't write files in directory %s\n", backup_dir);
        return 1;
    }
    else {
        fclose(out);
        unlink(test_file);
    }

    return 0;
}

int check_persistent_archive_dir() {
    char main_arch[256];
    char test_file[256];
    struct stat statb;
    FILE *out;

    get_backup_file_name (PERSISTENT_ARCH_DIR_NAME, main_arch, 255);

    if (stat (main_arch, &statb)) {
        /* directory isn't there, create it */
        if (mkdir (main_arch, 0777)) {
            /* Can't create directory */
            jp_logf (JP_LOG_WARN, "Can't create directory %s\n", main_arch);
            return 1;
        }
        if (stat (main_arch, &statb)) {
            jp_logf (JP_LOG_WARN, "Can't create directory %s\n", main_arch);
            return 1;
        }
    }

    /* Is it a directory? */
    if (!S_ISDIR(statb.st_mode)) {
        jp_logf(JP_LOG_WARN, "%s doesn't appear to be a directory.\n"
                    "I need it to be.\n", main_arch);
        return 1;
    }

    /* Can we write in it? */
    get_archive_file_name (main_arch, "test", test_file, 255);
    jp_logf (JP_LOG_DEBUG, "Trying to open %s for write\n", test_file);
    out = fopen (test_file, "w+");
    if (!out) {
        jp_logf (JP_LOG_WARN, "I can't write files in directory %s\n", main_arch);
        return 1;
    }
    else {
        fclose (out);
        unlink (test_file);
    }

    return 0;
}

/*
 * Appends the filename specified by file to the Backup directory path
 * and stores the result in full_name, unless the resulting string would
 * be greater than max_size.
 */
int get_backup_file_name (const char *file, char *full_name, int max_size) {
    char backup_dir[260];

    jp_get_home_file_name(BACKUP_DIR_NAME, backup_dir, 256);

    if (strlen (backup_dir) > (max_size - strlen (file) - 2)) {
        jp_logf (JP_LOG_WARN, "filename %s is too big\n", file);
        return 1;
    }

    sprintf (full_name, "%s/%s", backup_dir, file);
    return 0;
}

/*
 * Appends the filename specified by file to the filename specified by
 * arch, which is assumed to be the full path of a directory in the
 * Backup directory. The result is stored in full_name, unless the resulting
 * string would be greater than max_size.
 */
int get_archive_file_name (const char *arch, const char *file, char *full_name, int max_size) {

    if (strlen (arch) > (max_size - strlen (file) - 2)) {
        jp_logf (JP_LOG_WARN, "filename %s is too big\n", file);
        return 1;
    }

    sprintf (full_name, "%s/%s", arch, file);
    return 0;
}

/*
 * Moves the given items from one DBM file to the other.
 */
int dbm_move_items (char *src_name, char *dest_name, GList *node) {
    char src_dbf_name[256];
    char dest_dbf_name[256];
    GDBM_FILE src_dbf;
    GDBM_FILE dest_dbf;
    datum key;
    datum content;
    int ret;

    /* open the src dbm file */
    get_backup_file_name (src_name, src_dbf_name, 255);
    src_dbf = gdbm_open (src_dbf_name, 512, GDBM_WRCREAT | LOCK_FLAG, 0644, 0);
    if (!src_dbf) {
        /* Can't open or create dbm file */
        jp_logf (JP_LOG_FATAL,
                 "Can't open dbm file %s\nReason: %s\n",
                 src_dbf_name,
                 gdbm_strerror (gdbm_errno));
        return 1;
    }

    /* open the dest dbm file */
    get_backup_file_name (dest_name, dest_dbf_name, 255);
    dest_dbf = gdbm_open (dest_dbf_name, 512, GDBM_WRCREAT | LOCK_FLAG, 0644, 0);
    if (!dest_dbf) {
        /* Can't open or create dbm file */
        jp_logf (JP_LOG_FATAL,
                 "Can't open dbm file %s\nReason: %s\n",
                 dest_dbf_name,
                 gdbm_strerror (gdbm_errno));
        return 1;
    }

    while (node) {
        gchar *text = (gchar *)node->data;

        jp_logf (JP_LOG_DEBUG,
                     "dbm_move_items() - from: %s, to: %s, text: %s\n",
                     src_dbf_name,
                     dest_dbf_name,
                     text);

        key.dptr = text;
        key.dsize = strlen (text) + 1;

        /* fetch the content */
        content = gdbm_fetch (src_dbf, key);
        if (!content.dptr) {
            /* sanity check, shouldn't happen */
            jp_logf (JP_LOG_WARN, "Key %s has no content!\n", text);
            content.dptr = "0";
            content.dsize = 2;
        }

        /* store it in the dest dbm */
        ret = gdbm_store (dest_dbf, key, content, GDBM_INSERT);

        /* delete it from the src dbm */
        ret = gdbm_delete (src_dbf, key);

        node = node->next;
    }

    /* close the dbm files */
    gdbm_close (src_dbf);
    gdbm_close (dest_dbf);

    return 0;
}

void store_persistent_archive (const char *main_arch, const char *filename, gboolean replace) {
    char temp_str[256];
    struct stat statb;
    char *fncopy;
    char *bname;

    jp_logf (JP_LOG_DEBUG,
             "Backup: store_persistent_archive() - main_arch: [%s], filename: [%s], replace: [%d]\n",
             main_arch, filename, replace);

    fncopy = strdup (filename);
    bname = basename (fncopy);

    get_archive_file_name (main_arch, bname, temp_str, 255);

    /* see if it exists already */
    if (stat (temp_str, &statb) < 0) {
        if (errno != ENOENT) {
            /* Weird, we couldn't stat the file. Show an error message,
             * then go ahead and try and link the file anyway.
             */
            jp_logf (JP_LOG_WARN,
                     "Backup: Failed to stat %s - %s\n",
                     temp_str,
                     strerror (errno));
        }
    }
    else {
        /* The file exists.  See if we're supposed to replace it. */
        if (replace) {
            jp_logf (JP_LOG_DEBUG, "Backup: store_persistent_archive() - Replacing %s in %s\n",
                     bname, main_arch);
            if (unlink (temp_str)) {
                jp_logf (JP_LOG_WARN,
                         "Backup: Cannot replace link for %s in %s:\n%s\n",
                         bname, main_arch, strerror (errno));
            }
        }
        else {
            jp_logf (JP_LOG_DEBUG, "Backup: store_persistent_archive() - NOT replacing %s in %s\n",
                     bname, main_arch);
            free (fncopy);
            return;
        }
    }

    jp_logf (JP_LOG_DEBUG,
            "Backup: Creating link for %s in %s\n",
            bname, main_arch);
    if (link (filename, temp_str)) {
        jp_logf (JP_LOG_WARN,
                "Backup: Unable to create link for %s in %s:\n%s\n",
                bname, main_arch, strerror (errno));
    }

    free (fncopy);
}

static time_t get_last_backup_time (struct tm *timep) {
    char linkname[256];
    char archname[256];
    int count;
    int i, j, k;
    int x, y, z;

    if (!timep) return 0;

    get_backup_file_name ("LatestArchive", linkname, 255);
    count = readlink (linkname, archname, 255);
    if (count == -1) {
	if (errno != ENOENT) {
	    jp_logf (JP_LOG_FATAL, "%s\n", "Backup: Can't get last backup time.");
	}

	timep->tm_year = 1;
        return mktime (timep);
    }
    archname[count] = 0;

    count = sscanf (archname, "Archive_%4d-%2d-%2d@%2d:%2d:%2d", &i, &j, &k, &x, &y, &z);
    if (count != 6) {
	jp_logf (JP_LOG_FATAL, "%s\n", "Backup: Error scanning latest archive directory name.");
	timep->tm_year = 1;
        return mktime (timep);
    }

    timep->tm_year = i - 1900;
    timep->tm_mon = j - 1;
    timep->tm_mday = k;
    timep->tm_hour = x;
    timep->tm_min = y;
    timep->tm_sec = z;

    return mktime (timep);
}
