Chris@465: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@465: 
Chris@465: /*
Chris@465:     Sonic Visualiser
Chris@465:     An audio file viewer and annotation editor.
Chris@465:     Centre for Digital Music, Queen Mary, University of London.
Chris@465:     This file copyright 2008 QMUL.
Chris@465:     
Chris@465:     This program is free software; you can redistribute it and/or
Chris@465:     modify it under the terms of the GNU General Public License as
Chris@465:     published by the Free Software Foundation; either version 2 of the
Chris@465:     License, or (at your option) any later version.  See the file
Chris@465:     COPYING included with this distribution for more information.
Chris@465: */
Chris@465: 
Chris@465: #include "CachedFile.h"
Chris@465: 
Chris@465: #include "base/TempDirectory.h"
Chris@465: #include "base/ProgressReporter.h"
Chris@465: #include "base/Exceptions.h"
Chris@465: 
Chris@466: #include "FileSource.h"
Chris@466: 
Chris@465: #include <QFileInfo>
Chris@465: #include <QSettings>
Chris@465: #include <QVariant>
Chris@465: #include <QMap>
Chris@465: #include <QDir>
Chris@465: #include <QCryptographicHash>
Chris@465: 
Chris@481: #include "base/Profiler.h"
Chris@481: 
Chris@466: #include <iostream>
Chris@466: 
Chris@481: CachedFile::OriginLocalFilenameMap
Chris@481: CachedFile::m_knownGoodCaches;
Chris@481: 
Chris@465: QString
Chris@465: CachedFile::getLocalFilenameFor(QUrl url)
Chris@465: {
Chris@481:     Profiler p("CachedFile::getLocalFilenameFor");
Chris@481: 
Chris@465:     QDir dir(getCacheDirectory());
Chris@465: 
Chris@465:     QString filename =
Chris@465:         QString::fromLocal8Bit
Chris@465:         (QCryptographicHash::hash(url.toString().toLocal8Bit(),
Chris@465:                                   QCryptographicHash::Sha1).toHex());
Chris@465: 
Chris@465:     return dir.filePath(filename);
Chris@465: }
Chris@465: 
Chris@465: QString
Chris@465: CachedFile::getCacheDirectory()
Chris@465: {
Chris@465:     QDir dir = TempDirectory::getInstance()->getContainingPath();
Chris@465: 
Chris@465:     QString cacheDirName("cache");
Chris@465: 
Chris@465:     QFileInfo fi(dir.filePath(cacheDirName));
Chris@465: 
Chris@465:     if ((fi.exists() && !fi.isDir()) ||
Chris@465:         (!fi.exists() && !dir.mkdir(cacheDirName))) {
Chris@465: 
Chris@465:         throw DirectoryCreationFailed(fi.filePath());
Chris@465:     }
Chris@465: 
Chris@465:     return fi.filePath();
Chris@465: }
Chris@465: 
Chris@520: CachedFile::CachedFile(QString origin,
Chris@520:                        ProgressReporter *reporter,
Chris@520:                        QString preferredContentType) :
Chris@468:     m_origin(origin),
Chris@520:     m_preferredContentType(preferredContentType),
Chris@466:     m_reporter(reporter),
Chris@465:     m_ok(false)
Chris@465: {
Chris@481:     Profiler p("CachedFile::CachedFile[1]");
Chris@481: 
Chris@690:     SVDEBUG << "CachedFile::CachedFile: origin is \""
Chris@687:               << origin << "\"" << endl;
Chris@691:     checkFile();
Chris@467: }
Chris@467: 
Chris@520: CachedFile::CachedFile(QUrl url,
Chris@520:                        ProgressReporter *reporter,
Chris@520:     QString preferredContentType) :
Chris@468:     m_origin(url.toString()),
Chris@520:     m_preferredContentType(preferredContentType),
Chris@467:     m_reporter(reporter),
Chris@467:     m_ok(false)
Chris@467: {
Chris@481:     Profiler p("CachedFile::CachedFile[2]");
Chris@481: 
Chris@690:     SVDEBUG << "CachedFile::CachedFile: url is \""
Chris@687:               << url.toString() << "\"" << endl;
Chris@691:     checkFile();
Chris@467: }
Chris@467: 
Chris@467: CachedFile::~CachedFile()
Chris@467: {
Chris@465: }
Chris@465: 
Chris@465: bool
Chris@465: CachedFile::isOK() const
Chris@465: {
Chris@465:     return m_ok;
Chris@465: }
Chris@465: 
Chris@465: QString
Chris@465: CachedFile::getLocalFilename() const
Chris@465: {
Chris@465:     return m_localFilename;
Chris@465: }
Chris@465: 
Chris@465: void
Chris@691: CachedFile::checkFile()
Chris@465: {
Chris@465:     //!!! n.b. obvious race condition here if different CachedFile
Chris@465:     // objects for same url used in more than one thread -- need to
Chris@465:     // lock appropriately.  also consider race condition between
Chris@481:     // separate instances of the program!
Chris@481: 
Chris@481:     OriginLocalFilenameMap::const_iterator i = m_knownGoodCaches.find(m_origin);
Chris@481:     if (i != m_knownGoodCaches.end()) {
Chris@481:         m_ok = true;
Chris@481:         m_localFilename = i->second;
Chris@481:         return;
Chris@481:     }
Chris@481: 
Chris@481:     m_localFilename = getLocalFilenameFor(m_origin);
Chris@465: 
Chris@465:     if (!QFileInfo(m_localFilename).exists()) {
Chris@690:         SVDEBUG << "CachedFile::check: Local file does not exist, making a note that it hasn't been retrieved" << endl;
Chris@465:         updateLastRetrieval(false); // empirically!
Chris@465:     }
Chris@465: 
Chris@465:     QDateTime lastRetrieval = getLastRetrieval();
Chris@465: 
Chris@465:     if (lastRetrieval.isValid()) {
Chris@690:         SVDEBUG << "CachedFile::check: Valid last retrieval at "
Chris@687:                   << lastRetrieval.toString() << endl;
Chris@465:         // this will not be the case if the file is missing, after
Chris@465:         // updateLastRetrieval(false) was called above
Chris@465:         m_ok = true;
Chris@465:         if (lastRetrieval.addDays(2) < QDateTime::currentDateTime()) { //!!!
Chris@690:             SVDEBUG << "CachedFile::check: Out of date; trying to retrieve again" << endl;
Chris@465:             // doesn't matter if retrieval fails -- we just don't
Chris@465:             // update the last retrieval time
Chris@465: 
Chris@465:             //!!! but we do want an additional last-attempted
Chris@465:             // timestamp so as to ensure we aren't retrying the
Chris@465:             // retrieval every single time if it isn't working
Chris@465: 
Chris@465:             if (retrieve()) {
Chris@690:                 SVDEBUG << "CachedFile::check: Retrieval succeeded" << endl;
Chris@465:                 updateLastRetrieval(true);
Chris@467:             } else {
Chris@843:                 cerr << "CachedFile::check: Retrieval failed, will try again later (using existing file for now)" << endl;
Chris@467:             }                
Chris@465:         }
Chris@465:     } else {
Chris@690:         SVDEBUG << "CachedFile::check: No valid last retrieval" << endl;
Chris@465:         // there is no acceptable file
Chris@465:         if (retrieve()) {
Chris@690:             SVDEBUG << "CachedFile::check: Retrieval succeeded" << endl;
Chris@465:             m_ok = true;
Chris@465:             updateLastRetrieval(true);
Chris@465:         } else {
Chris@843:             cerr << "CachedFile::check: Retrieval failed, remaining in invalid state" << endl;
Chris@465:             // again, we don't need to do anything here -- the last
Chris@465:             // retrieval timestamp is already invalid
Chris@465:         }
Chris@465:     }
Chris@481: 
Chris@481:     if (m_ok) {
Chris@481:         m_knownGoodCaches[m_origin] = m_localFilename;
Chris@481:     }
Chris@465: }
Chris@465: 
Chris@465: bool
Chris@465: CachedFile::retrieve()
Chris@465: {
Chris@465:     //!!! need to work by retrieving the file to another name, and
Chris@465:     //!!! then "atomically" moving it to its proper place (I'm not
Chris@465:     //!!! sure we can do an atomic move to replace an existing file
Chris@465:     //!!! using Qt classes, but a plain delete then copy is probably
Chris@465:     //!!! good enough)
Chris@465: 
Chris@520:     FileSource fs(m_origin, m_reporter, m_preferredContentType);
Chris@465: 
Chris@466:     if (!fs.isOK() || !fs.isAvailable()) {
Chris@690:         SVDEBUG << "CachedFile::retrieve: ERROR: FileSource reported unavailable or failure" << endl;
Chris@466:         return false;
Chris@466:     }
Chris@465: 
Chris@466:     fs.waitForData();
Chris@465: 
Chris@466:     if (!fs.isOK()) {
Chris@690:         SVDEBUG << "CachedFile::retrieve: ERROR: FileSource reported failure during receive" << endl;
Chris@466:         return false;
Chris@466:     }
Chris@466: 
Chris@466:     QString tempName = fs.getLocalFilename();
Chris@466:     QFile tempFile(tempName);
Chris@466:     if (!tempFile.exists()) {
Chris@690:         SVDEBUG << "CachedFile::retrieve: ERROR: FileSource reported success, but local temporary file \"" << tempName << "\" does not exist" << endl;
Chris@466:         return false;
Chris@466:     }
Chris@466: 
Chris@466:     QFile previous(m_localFilename);
Chris@466:     if (previous.exists()) {
Chris@466:         if (!previous.remove()) {
Chris@843:             cerr << "CachedFile::retrieve: ERROR: Failed to remove previous copy of cached file at \"" << m_localFilename << "\"" << endl;
Chris@466:             return false;
Chris@466:         }
Chris@466:     }
Chris@466: 
Chris@466:     //!!! This is not ideal, could leave us with nothing (old file
Chris@466:     //!!! removed, new file not able to be copied in because e.g. no
Chris@466:     //!!! disk space left)
Chris@466: 
Chris@466:     if (!tempFile.copy(m_localFilename)) {
Chris@843:         cerr << "CachedFile::retrieve: ERROR: Failed to copy newly retrieved file from \"" << tempName << "\" to \"" << m_localFilename << "\"" << endl;
Chris@466:         return false;
Chris@466:     }
Chris@466: 
Chris@690:     SVDEBUG << "CachedFile::retrieve: Successfully copied newly retrieved file \"" << tempName << "\" to its home at \"" << m_localFilename << "\"" << endl;
Chris@468: 
Chris@466:     return true;
Chris@465: }
Chris@465: 
Chris@465: QDateTime
Chris@465: CachedFile::getLastRetrieval()
Chris@465: {
Chris@465:     QSettings settings;
Chris@465:     settings.beginGroup("FileCache");
Chris@465: 
Chris@465:     QString key("last-retrieval-times");
Chris@465: 
Chris@465:     QMap<QString, QVariant> timeMap = settings.value(key).toMap();
Chris@465:     QDateTime lastTime = timeMap[m_localFilename].toDateTime();
Chris@465: 
Chris@465:     settings.endGroup();
Chris@465:     return lastTime;
Chris@465: }
Chris@465: 
Chris@465: void
Chris@465: CachedFile::updateLastRetrieval(bool successful)
Chris@465: {
Chris@465:     //!!! note !successful does not mean "we failed to update the
Chris@465:     //!!! file" (and so it remains the same as before); it means "the
Chris@465:     //!!! file is not there at all"
Chris@465:     
Chris@465:     QSettings settings;
Chris@465:     settings.beginGroup("FileCache");
Chris@465: 
Chris@465:     QString key("last-retrieval-times");
Chris@465: 
Chris@465:     QMap<QString, QVariant> timeMap = settings.value(key).toMap();
Chris@465: 
Chris@465:     QDateTime dt;
Chris@465:     if (successful) dt = QDateTime::currentDateTime();
Chris@465: 
Chris@465:     timeMap[m_localFilename] = dt;
Chris@465:     settings.setValue(key, timeMap);
Chris@465: 
Chris@465:     settings.endGroup();
Chris@465: }
Chris@465: 
Chris@465: