Blob Blame History Raw
/*
    Copyright (c) 2017, Lukas Holecek <hluk@email.cz>

    This file is part of CopyQ.

    CopyQ 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 3 of the License, or
    (at your option) any later version.

    CopyQ 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 CopyQ.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "itemstore.h"

#include "common/config.h"
#include "common/log.h"
#include "common/textdata.h"
#include "item/itemfactory.h"

#include <QAbstractItemModel>
#include <QDir>
#include <QFile>

namespace {

/// @return File name for data file with items.
QString itemFileName(const QString &id)
{
    QString part( id.toUtf8().toBase64() );
    part.replace( QChar('/'), QString('-') );
    return getConfigurationFilePath("_tab_") + part + QString(".dat");
}

bool createItemDirectory()
{
    QDir settingsDir( settingsDirectoryPath() );
    if ( !settingsDir.mkpath(".") ) {
        log( QString("Cannot create directory for settings %1!")
             .arg(quoteString(settingsDir.path()) ),
             LogError );

        return false;
    }

    return true;
}

void printItemFileError(
        const QString &action, const QString &id, const QString &fileName, const QFile &file)
{
    log( QString("Cannot %1 tab %2 to %3 (%4)!")
         .arg(action)
         .arg( quoteString(id) )
         .arg( quoteString(fileName) )
         .arg( file.errorString() )
         , LogError );
}

void printSaveItemFileError(const QString &id, const QString &fileName, const QFile &file)
{
    printItemFileError("save", id, fileName, file);
}

void printLoadItemFileError(const QString &id, const QString &fileName, const QFile &file)
{
    printItemFileError("load", id, fileName, file);
}

ItemSaverPtr loadItems(
        const QString &tabName, const QString &tabFileName,
        QAbstractItemModel &model, ItemFactory *itemFactory, int maxItems)
{
    COPYQ_LOG( QString("Tab \"%1\": Loading items").arg(tabName) );

    QFile tabFile(tabFileName);
    if ( !tabFile.open(QIODevice::ReadOnly) ) {
        printLoadItemFileError(tabName, tabFileName, tabFile);
        return nullptr;
    }

    auto loader = itemFactory->loadItems(tabName, &model, &tabFile, maxItems);
    if (!loader) {
        const QString errorString =
                QObject::tr("Item file %1 is corrupted or some CopyQ plugins are missing!")
                .arg( quoteString(tabFileName) );
        itemFactory->emitError(errorString);
        return nullptr;
    }

    return loader;
}

ItemSaverPtr createTab(
        const QString &tabName, QAbstractItemModel &model, ItemFactory *itemFactory, int maxItems)
{
    COPYQ_LOG( QString("Tab \"%1\": Creating new tab").arg(tabName) );

    auto saver = itemFactory->initializeTab(tabName, &model, maxItems);
    if (!saver) {
        log( QString("Tab \"%1\": Failed to create new tab"), LogError );
        return nullptr;
    }

    if ( !saveItems(tabName, model, saver) )
        return nullptr;

    return saver;
}

} // namespace

ItemSaverPtr loadItems(const QString &tabName, QAbstractItemModel &model, ItemFactory *itemFactory, int maxItems)
{
    if ( !createItemDirectory() )
        return nullptr;

    const QString tabFileName = itemFileName(tabName);

    // If tab file doesn't exist, try to restore data from temporary file.
    if ( !QFile::exists(tabFileName) ) {
        QFile tmpFile(tabFileName + ".tmp");
        if ( tmpFile.exists() ) {
            log( QString("Tab \"%1\": Restoring items (previous save failed)"), LogWarning );
            if ( !tmpFile.rename(tabFileName) ) {
                printLoadItemFileError(tabName, tabFileName, tmpFile);
                return nullptr;
            }
        }
    }

    // Load file with items or create new file.
    auto saver = QFile::exists(tabFileName)
            ? loadItems(tabName, tabFileName, model, itemFactory, maxItems)
            : createTab(tabName, model, itemFactory, maxItems);

    if (!saver) {
        model.removeRows(0, model.rowCount());
        return nullptr;
    }

    COPYQ_LOG( QString("Tab \"%1\": %2 items loaded").arg(tabName).arg(model.rowCount()) );

    return saver;
}

bool saveItems(const QString &tabName, const QAbstractItemModel &model, const ItemSaverPtr &saver)
{
    const QString tabFileName = itemFileName(tabName);

    if ( !createItemDirectory() )
        return false;

    // Save to temp file.
    QFile tmpFile( tabFileName + ".tmp" );
    if ( !tmpFile.open(QIODevice::WriteOnly) ) {
        printSaveItemFileError(tabName, tabFileName, tmpFile);
        return false;
    }

    COPYQ_LOG( QString("Tab \"%1\": Saving %2 items").arg(tabName).arg(model.rowCount()) );

    if ( !saver->saveItems(tabName, model, &tmpFile) ) {
        COPYQ_LOG( QString("Tab \"%1\": Failed to save items!").arg(tabName) );
        return false;
    }

    // 1. Safely flush all data to temporary file.
    tmpFile.flush();

    // 2. Remove old tab file.
    {
        QFile oldTabFile(tabFileName);
        if (oldTabFile.exists() && !oldTabFile.remove()) {
            printSaveItemFileError(tabName, tabFileName, oldTabFile);
            return false;
        }
    }

    // 3. Overwrite previous file.
    if ( !tmpFile.rename(tabFileName) ) {
        printSaveItemFileError(tabName, tabFileName, tmpFile);
        return false;
    }

    COPYQ_LOG( QString("Tab \"%1\": Items saved").arg(tabName) );

    return true;
}

void removeItems(const QString &tabName)
{
    const QString tabFileName = itemFileName(tabName);
    QFile::remove(tabFileName);
    QFile::remove(tabFileName + ".tmp");
}

void moveItems(const QString &oldId, const QString &newId)
{
    const QString oldFileName = itemFileName(oldId);
    const QString newFileName = itemFileName(newId);

    if ( oldFileName != newFileName && QFile::copy(oldFileName, newFileName) ) {
        QFile::remove(oldFileName);
    } else {
        COPYQ_LOG( QString("Failed to move items from \"%1\" (tab \"%2\") to \"%3\" (tab \"%4\")")
                   .arg(oldFileName).arg(oldId)
                   .arg(newFileName).arg(newId) );
    }
}