/*
 * virfilecache.c: file caching for data
 *
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 */


#include <config.h>

#include "internal.h"

#include "viralloc.h"
#include "virbuffer.h"
#include "vircrypto.h"
#include "virerror.h"
#include "virfile.h"
#include "virfilecache.h"
#include "virhash.h"
#include "virlog.h"
#include "virobject.h"
#include "virstring.h"

#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define VIR_FROM_THIS VIR_FROM_NONE

VIR_LOG_INIT("util.filecache")


struct _virFileCache {
    virObjectLockable object;

    virHashTablePtr table;

    char *dir;
    char *suffix;

    void *priv;

    virFileCacheHandlers handlers;
};


static virClassPtr virFileCacheClass;


static void
virFileCachePrivFree(virFileCachePtr cache)
{
    if (cache->priv && cache->handlers.privFree)
        cache->handlers.privFree(cache->priv);
}


static void
virFileCacheDispose(void *obj)
{
    virFileCachePtr cache = obj;

    VIR_FREE(cache->dir);
    VIR_FREE(cache->suffix);

    virHashFree(cache->table);

    virFileCachePrivFree(cache);
}


static int
virFileCacheOnceInit(void)
{
    if (!(virFileCacheClass = virClassNew(virClassForObjectLockable(),
                                          "virFileCache",
                                          sizeof(virFileCache),
                                          virFileCacheDispose)))
        return -1;

    return 0;
}


VIR_ONCE_GLOBAL_INIT(virFileCache)


static char *
virFileCacheGetFileName(virFileCachePtr cache,
                        const char *name)
{
    char *file = NULL;
    char *namehash = NULL;
    virBuffer buf = VIR_BUFFER_INITIALIZER;

    if (virCryptoHashString(VIR_CRYPTO_HASH_SHA256, name, &namehash) < 0)
        goto cleanup;

    if (virFileMakePath(cache->dir) < 0) {
        virReportSystemError(errno,
                             _("Unable to create directory '%s'"),
                             cache->dir);
        goto cleanup;
    }

    virBufferAsprintf(&buf, "%s/%s", cache->dir, namehash);

    if (cache->suffix)
        virBufferAsprintf(&buf, ".%s", cache->suffix);

    if (virBufferCheckError(&buf) < 0)
        goto cleanup;

    file = virBufferContentAndReset(&buf);

 cleanup:
    VIR_FREE(namehash);
    return file;
}


static int
virFileCacheLoad(virFileCachePtr cache,
                 const char *name,
                 void **data)
{
    char *file = NULL;
    int ret = -1;
    void *loadData = NULL;

    *data = NULL;

    if (!(file = virFileCacheGetFileName(cache, name)))
        return ret;

    if (!virFileExists(file)) {
        if (errno == ENOENT) {
            VIR_DEBUG("No cached data '%s' for '%s'", file, name);
            ret = 0;
            goto cleanup;
        }
        virReportSystemError(errno,
                             _("Unable to access cache '%s' for '%s'"),
                             file, name);
        goto cleanup;
    }

    if (!(loadData = cache->handlers.loadFile(file, name, cache->priv))) {
        virErrorPtr err = virGetLastError();
        VIR_WARN("Failed to load cached data from '%s' for '%s': %s",
                 file, name, err ? NULLSTR(err->message) : "unknown error");
        virResetLastError();
        ret = 0;
        goto cleanup;
    }

    if (!cache->handlers.isValid(loadData, cache->priv)) {
        VIR_DEBUG("Outdated cached capabilities '%s' for '%s'", file, name);
        ignore_value(unlink(file));
        ret = 0;
        goto cleanup;
    }

    VIR_DEBUG("Loaded cached data '%s' for '%s'", file, name);

    ret = 1;
    VIR_STEAL_PTR(*data, loadData);

 cleanup:
    virObjectUnref(loadData);
    VIR_FREE(file);
    return ret;
}


static int
virFileCacheSave(virFileCachePtr cache,
                 const char *name,
                 void *data)
{
    char *file = NULL;
    int ret = -1;

    if (!(file = virFileCacheGetFileName(cache, name)))
        return ret;

    if (cache->handlers.saveFile(data, file, cache->priv) < 0)
        goto cleanup;

    ret = 0;

 cleanup:
    VIR_FREE(file);
    return ret;
}


static void *
virFileCacheNewData(virFileCachePtr cache,
                    const char *name)
{
    void *data = NULL;
    int rv;

    if ((rv = virFileCacheLoad(cache, name, &data)) < 0)
        return NULL;

    if (rv == 0) {
        if (!(data = cache->handlers.newData(name, cache->priv)))
            return NULL;

        if (virFileCacheSave(cache, name, data) < 0) {
            virObjectUnref(data);
            data = NULL;
        }
    }

    return data;
}


/**
 * virFileCacheNew:
 * @dir: the cache directory where all the cache files will be stored
 * @suffix: the cache file suffix or NULL if no suffix is required
 * @handlers: filled structure with all required handlers
 *
 * Creates a new cache object which handles caching any data to files
 * stored on a filesystem.
 *
 * Returns new cache object or NULL on error.
 */
virFileCachePtr
virFileCacheNew(const char *dir,
                const char *suffix,
                virFileCacheHandlers *handlers)
{
    virFileCachePtr cache;

    if (virFileCacheInitialize() < 0)
        return NULL;

    if (!(cache = virObjectNew(virFileCacheClass)))
        return NULL;

    if (!(cache->table = virHashCreate(10, virObjectFreeHashData)))
        goto cleanup;

    if (VIR_STRDUP(cache->dir, dir) < 0)
        goto cleanup;

    if (VIR_STRDUP(cache->suffix, suffix) < 0)
        goto cleanup;

    cache->handlers = *handlers;

    return cache;

 cleanup:
    virObjectUnref(cache);
    return NULL;
}


static void
virFileCacheValidate(virFileCachePtr cache,
                     const char *name,
                     void **data)
{
    if (*data && !cache->handlers.isValid(*data, cache->priv)) {
        VIR_DEBUG("Cached data '%p' no longer valid for '%s'",
                  *data, NULLSTR(name));
        if (name)
            virHashRemoveEntry(cache->table, name);
        *data = NULL;
    }

    if (!*data && name) {
        VIR_DEBUG("Creating data for '%s'", name);
        *data = virFileCacheNewData(cache, name);
        if (*data) {
            VIR_DEBUG("Caching data '%p' for '%s'", *data, name);
            if (virHashAddEntry(cache->table, name, *data) < 0) {
                virObjectUnref(*data);
                *data = NULL;
            }
        }
    }
}


/**
 * virFileCacheLookup:
 * @cache: existing cache object
 * @name: name of the data stored in a cache
 *
 * Lookup a data specified by name.  This tries to find a file with
 * cached data, if it doesn't exist or is no longer valid new data
 * is created.
 *
 * Returns data object or NULL on error.  The caller is responsible for
 * unrefing the data.
 */
void *
virFileCacheLookup(virFileCachePtr cache,
                   const char *name)
{
    void *data = NULL;

    virObjectLock(cache);

    data = virHashLookup(cache->table, name);
    virFileCacheValidate(cache, name, &data);

    virObjectRef(data);
    virObjectUnlock(cache);

    return data;
}


/**
 * virFileCacheLookupByFunc:
 * @cache: existing cache object
 * @iter: an iterator to identify the desired data
 * @iterData: extra opaque information passed to the @iter
 *
 * Similar to virFileCacheLookup() except it search by @iter.
 *
 * Returns data object or NULL on error.  The caller is responsible for
 * unrefing the data.
 */
void *
virFileCacheLookupByFunc(virFileCachePtr cache,
                         virHashSearcher iter,
                         const void *iterData)
{
    void *data = NULL;
    char *name = NULL;

    virObjectLock(cache);

    data = virHashSearch(cache->table, iter, iterData, (void **)&name);
    virFileCacheValidate(cache, name, &data);

    virObjectRef(data);
    virObjectUnlock(cache);

    VIR_FREE(name);

    return data;
}


/**
 * virFileCacheGetPriv:
 * @cache: existing cache object
 *
 * Returns private data used by @handlers.
 */
void *
virFileCacheGetPriv(virFileCachePtr cache)
{
    void *priv;

    virObjectLock(cache);

    priv = cache->priv;

    virObjectUnlock(cache);

    return priv;
}


/**
 * virFileCacheSetPriv:
 * @cache: existing cache object
 * @priv: private data to set
 *
 * Sets private data used by @handlers.  If there is already some @priv
 * set, privFree() will be called on the old @priv before setting a new one.
 */
void
virFileCacheSetPriv(virFileCachePtr cache,
                    void *priv)
{
    virObjectLock(cache);

    virFileCachePrivFree(cache);

    cache->priv = priv;

    virObjectUnlock(cache);
}


/**
 * virFileCacheInsertData:
 * @cache: existing cache object
 * @name: name of the new data
 * @data: the actual data object to store in cache
 *
 * Adds a new data into a cache but doesn't store the data into
 * a file.  This function should be used only by testing code.
 *
 * Returns 0 on success, -1 on error.
 */
int
virFileCacheInsertData(virFileCachePtr cache,
                       const char *name,
                       void *data)
{
    int ret;

    virObjectLock(cache);

    ret = virHashUpdateEntry(cache->table, name, data);

    virObjectUnlock(cache);

    return ret;
}
