akabeicache.cpp 10.1 KB
Newer Older
1
/*
2 3 4 5 6 7 8 9 10 11 12
 * This file is part of the Chakra project
 * The cache manager

   Copyright (C) 2010 Lisa Vitolo <shainer@chakra-project.org>

   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.
*/

13 14 15 16
#include <akabeicache.h>
#include <akabeidatabase.h>
#include <akabeipackage.h>
#include <akabeiconfig.h>
Lukas Appelhans's avatar
Lukas Appelhans committed
17
#include <akabeihelpers.h>
18

Lukas Appelhans's avatar
Lukas Appelhans committed
19
#include <utime.h>
Lisa's avatar
Lisa committed
20
#include <libarchive++/archivehandler.h>
Lukas Appelhans's avatar
Lukas Appelhans committed
21
#include <SQLiteConnection.h>
Lisa's avatar
Lisa committed
22
#include "akabeiquery.h"
Lukas Appelhans's avatar
Lukas Appelhans committed
23

24 25 26 27 28
/*
 * This is an utility class we use for sorting the cache entries according to some date/time.
 */
namespace Akabei
{
29 30 31
/**
 * @class CachePair
 * @brief Class for data pairs with a date and a file path.
32
 */
33 34 35 36 37 38 39
class CachePair
{
public:
    CachePair(QDateTime key, QString value)
        : m_key(key)
        , m_value(value)
    {}
40

41 42 43
    /*
     * qSort() needs this operator to function
     */
44
    bool operator<(CachePair const& other) const
45 46 47
    {
        return (m_key < other.m_key); /* we do a date comparison */
    }
48

49 50 51 52
    QDateTime key() const
    {
        return m_key;
    }
53

54 55 56 57
    QString value() const
    {
        return m_value;
    }
58

59 60 61 62 63
private:
    QDateTime m_key;
    QString m_value;
};

64 65 66
class Cache::Private
{
public:
67 68
    Private(const QDir &cd, const QDir &dd) : cachedir(cd), databasedir(dd), plainEntryList(cd.entryList()) {}
    Private(const QDir &cd, const QDir &dd, const QStringList &kp) : cachedir(cd), databasedir(dd), plainEntryList(cd.entryList()), keepPackages(kp) {}
69
    ~Private() {}
Lukas Appelhans's avatar
Lukas Appelhans committed
70

71 72 73 74
    enum RemoveCriteria {
        CreationDate,
        LastReadDate
    };
Lukas Appelhans's avatar
Lukas Appelhans committed
75

76
    void init();
Lukas Appelhans's avatar
Lukas Appelhans committed
77

78 79 80
    QString getPackageName(const QString &);
    QString getPackageVersion(const QString &);
    qint64 totalCacheSize();
Lukas Appelhans's avatar
Lukas Appelhans committed
81

Lukas Appelhans's avatar
Lukas Appelhans committed
82
    bool cleanAll();
83
    bool cleanConditional(RemoveCriteria criteria, bool half = false);
84
    bool cleanNotInstalled();
Lukas Appelhans's avatar
Lukas Appelhans committed
85

86
    QDir cachedir;
87
    QDir databasedir;
88 89 90 91 92 93 94
    QStringList cacheFiles;
    QStringList plainEntryList;
    QStringList keepPackages;
    QDateTime weeks2older;
    QString error;
};

95 96 97 98
/*
 * This costructor is meant to be used by all the classes that needs to write/get a file from the cache.
 * All the options are read from the configuration file later.
 */
99
Cache::Cache()
100
    : d(new Private(Config::instance()->cacheDir(), Config::instance()->databaseDir()))
101
{
102 103
    d->init();
    d->keepPackages.append(Config::instance()->keepPackages());
104 105 106 107 108 109
}

/*
 * This constructor is meant to be used by the akabei-clean-cache tool, where the options
 * can be specified from the command line.
 */
110 111
Cache::Cache(const QString &cachedir, const QString &databasedir, const QStringList &keepPackages)
    : d(new Private(cachedir, databasedir, keepPackages))
112
{
113
    d->init();
114 115
}

Lukas Appelhans's avatar
Lukas Appelhans committed
116 117 118 119 120
Cache::~Cache()
{
    delete d;
}

121
void Cache::Private::init()
122
{
123 124
    /*
     * Create a new list of cache entries with their full path.
Lukas Appelhans's avatar
Lukas Appelhans committed
125
     */
126
    foreach (QString const& cacheFile, plainEntryList) {
127
        if (!cacheFile.startsWith(QLatin1String("."))) {
128
            cacheFiles.append(cachedir.absoluteFilePath(cacheFile));
129 130
        }
    }
131

132 133 134 135 136 137 138
    /*
     * Find the date exactly 2 weeks earlier than the current day.
     * This is useful later because the most "old" or "unused" packages are actually removed
     * only if they are older (or have not been accessed for more than) 2 weeks, in order to prevent
     * quite recent files to be removed.
     */
    QDateTime current = QDateTime::currentDateTime();
139
    weeks2older = current.addDays(-14);
140 141
}

142
bool Cache::isPackageInCache(const QString &filename) const
143
{
144
    return d->plainEntryList.contains(filename);
145 146
}

147 148 149 150
/*
 * Get the path associated with the filename requested,
 * or an empty string if the filename isn't in the cache.
 */
151
QString Cache::getPathInCache(const QString &filename) const
152
{
153
    /*
Lukas Appelhans's avatar
Lukas Appelhans committed
154
     * We now update the "last read date" of the file, so that we can keep track of which files
155 156
     * were requested a longer time ago (see the RemoveCriteria enumeration).
     */
Lukas Appelhans's avatar
Lukas Appelhans committed
157
    struct utimbuf time;
158 159
    time.modtime = QDateTime::currentDateTimeUtc().toTime_t();
    time.actime = QDateTime::currentDateTimeUtc().toTime_t();
Lukas Appelhans's avatar
Lukas Appelhans committed
160 161 162
    std::string tmp = filename.toStdString();
    utime(tmp.c_str(), &time);

163
    return d->cachedir.absoluteFilePath(filename);
164 165
}

166
QStringList Cache::getPackagesByName(const QString &pkg) const
167 168
{
    QStringList matches;
169

170
    foreach (QString const& file, d->plainEntryList) {
171
        if (d->getPackageName(file) == pkg && file.endsWith(Akabei::packageExtension)) {
172
            matches.append(d->cachedir.absoluteFilePath(file));
173 174
        }
    }
175

176 177 178
    return matches;
}

179 180 181 182
/*
 * Writes a new file on cache
 * checking if the maximum size has been reached and acting appropriately if so.
 */
183
bool Cache::writePackage(const QString &path, const QString &newName)
184
{
Lukas Appelhans's avatar
Lukas Appelhans committed
185
    if (path != newName && !Helpers::copyFile(path, d->cachedir.absoluteFilePath(newName))) { //FIXME: Port to polkit
186
        d->error = "Could not copy \"" + path + "\" to cache.\n";
187 188
        return false;
    }
189

190 191
    /* Convert max size in bytes */
    qint64 maxsize = (Config::instance()->maxCacheSize()) * 1024 * 1024;
192

193
    if (maxsize > 0 && d->totalCacheSize() >= maxsize) {
Lukas Appelhans's avatar
Lukas Appelhans committed
194
        switch (Config::instance()->cachePolicy()) {
195 196 197 198
            case CleanAll:
                return d->cleanAll(); /* no need to go on after this... */

            case CleanOld:
Lukas Appelhans's avatar
Lukas Appelhans committed
199
                return d->cleanConditional(d->CreationDate, true);
200 201

            case CleanUnused:
Lukas Appelhans's avatar
Lukas Appelhans committed
202 203
                return d->cleanConditional(d->LastReadDate, true);

204
            case CleanOldAndUnused: {
Lukas Appelhans's avatar
Lukas Appelhans committed
205 206
                bool worked = d->cleanConditional(Cache::Private::CreationDate);
                return worked && d->cleanConditional(Cache::Private::LastReadDate);
207 208 209
            }
            case CleanNotInstalled:
                return d->cleanNotInstalled();
210 211
        }
    }
212

213 214 215
    return true;
}

Lukas Appelhans's avatar
Lukas Appelhans committed
216
void Cache::cleanCache(MaxSizePolicy policy)
217
{
Lukas Appelhans's avatar
Lukas Appelhans committed
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
    /*
     * Decides the operation to execute
     * (more than one can be executed in this same invocation, except for CleanAll of course)
     */
    switch (policy) {
        case CleanAll:
            d->cleanAll();
            break;
        case CleanOldAndUnused:
            d->cleanConditional(Cache::Private::CreationDate);
            d->cleanConditional(Cache::Private::LastReadDate);
            break;
        case CleanOld:
            d->cleanConditional(Cache::Private::CreationDate);
            break;
        case CleanUnused:
            d->cleanConditional(Cache::Private::LastReadDate);
            break;
236 237 238
        case CleanNotInstalled:
            d->cleanNotInstalled();
            break;
Lukas Appelhans's avatar
Lukas Appelhans committed
239
    };
240 241
}

242 243 244
bool Cache::Private::cleanNotInstalled()
{
    try {
245
        SQLiteConnection database(databasedir.absoluteFilePath(QStringLiteral("local.db")), true);
246
        foreach (QString const& file, cacheFiles) {
247 248
            database.bind(QStringLiteral(":Filename"), file.split(QStringLiteral("/")).last());
            SQLiteResource res = database.query( Queries::selectPackages(QStringLiteral("filename"), QStringLiteral("="), QStringLiteral(":Filename")) );
249
            if (res.getRowsCount() == 0) {
Lukas Appelhans's avatar
Lukas Appelhans committed
250 251
                if (!Helpers::removeFile(file)) {
                    error = QObject::tr("Failed to remove file!");
252 253 254 255 256
                    return false;
                }
            }
        }
    } catch (SQLiteException &exc) {
257
        qWarning() << "Caught an error with" << cachedir.absoluteFilePath(QStringLiteral("local.db")) << ":" << exc.what();
258 259 260 261 262
        return false;
    }
    return true;
}

263
/*
264 265 266
 * The bool parameter is needed because to distinguish whether we're removing packages because of an explicit
 * request from the user (in that case, it removes all the old/unused packages) or if we're removing because
 * the cache has exceeded the maximum size allowed. In this second case, it halves the size (at most).
267
 */
268
bool Cache::Private::cleanConditional(RemoveCriteria criteria, bool half)
269
{
270
    int newCacheSize( (half) ? cacheFiles.size() / 2 : cacheFiles.size() );
271
    QList<CachePair> timedFiles;
272

273
    foreach (QString const& file, cacheFiles) {
274
        QFileInfo fileInfo(file);
275
        CachePair pair((criteria == CreationDate) ? fileInfo.created() : fileInfo.lastRead(), file);
276 277 278 279
        timedFiles.append(pair);
    }

    qSort(timedFiles);
280 281

    foreach (CachePair const& pair, timedFiles.mid(0, newCacheSize)) {
282
        if (pair.key() >= weeks2older || keepPackages.contains(cachedir.relativeFilePath(pair.value()))) {
283 284
            continue;
        }
285

Lukas Appelhans's avatar
Lukas Appelhans committed
286 287
        if (!Helpers::removeFile(pair.value())) {
            error = QObject::tr("Failed to remove file!");
288 289 290
            return false;
        }
    }
291

292 293 294
    return true;
}

295
bool Cache::Private::cleanAll()
296
{
297
    foreach (QString const& file, plainEntryList) {
298
        QString packageName = getPackageName(file);
299

300
        if (file.startsWith(QLatin1String(".")) || keepPackages.contains(packageName)) {
301 302 303
            continue;
        }

Lukas Appelhans's avatar
Lukas Appelhans committed
304 305
        if (!Helpers::removeFile(cachedir.absoluteFilePath(file))) {
            error = QObject::tr("Failed to remove file!");
306 307 308
            return false;
        }
    }
309

310 311 312 313 314 315 316
    return true;
}

/*
 * The following two functions separate the filename of a package, in the form
 * package-version, into name and version.
 */
317
QString Cache::Private::getPackageName(const QString& filename)
318
{
319 320 321
    QString pkgname;

    foreach (QString const& token, filename.split("-")) {
322 323 324
        if (token.isEmpty() || token[0].isNumber()) {
            break;
        }
325

326
        pkgname += token;
327
        pkgname += QLatin1String("-");
328
    }
329 330

    pkgname.resize(pkgname.size() - 1);
331 332 333
    return pkgname;
}

334
QString Cache::Private::getPackageVersion(const QString& filename)
335
{
336 337 338
    QString pkgver;

    foreach (QString const& token, filename.split("-")) {
339 340 341
        if (token.isEmpty() || !token[0].isNumber()) {
            continue;
        }
342

343
        pkgver += token;
344
        pkgver += QLatin1String("-");
345
    }
346 347

    pkgver.resize(pkgver.size() - 1);
348 349 350
    return pkgver;
}

351
qint64 Cache::Private::totalCacheSize()
352
{
353 354
    qint64 total( 0 );

355
    foreach (QString const& filename, cacheFiles) {
356
        total += QFile( filename ).size();
357
    }
358

359 360 361
    return total;
}

362
QString Cache::errorString() const
363
{
364
    return d->error;
365 366
}

Lisa's avatar
Lisa committed
367
}