Chris@679: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@679: 
Chris@679: /*
Chris@679:     Sonic Visualiser
Chris@679:     An audio file viewer and annotation editor.
Chris@679:     Centre for Digital Music, Queen Mary, University of London.
Chris@679:     
Chris@679:     This program is free software; you can redistribute it and/or
Chris@679:     modify it under the terms of the GNU General Public License as
Chris@679:     published by the Free Software Foundation; either version 2 of the
Chris@679:     License, or (at your option) any later version.  See the file
Chris@679:     COPYING included with this distribution for more information.
Chris@679: */
Chris@679: 
Chris@679: /*
Chris@679:    This is a modified version of a source file from the 
Chris@679:    Rosegarden MIDI and audio sequencer and notation editor.
Chris@679:    This file copyright 2005-2011 Chris Cannam and the Rosegarden
Chris@679:    development team.
Chris@679: */
Chris@679: 
Chris@679: #include "ResourceFinder.h"
Chris@679: 
Chris@679: #include <QDir>
Chris@679: #include <QFileInfo>
Chris@679: #include <QStringList>
Chris@679: #include <QProcess>
Chris@679: #include <QCoreApplication>
Chris@679: 
Chris@982: #if QT_VERSION >= 0x050000
Chris@982: #include <QStandardPaths>
Chris@982: #endif
Chris@982: 
Chris@679: #include <cstdlib>
Chris@679: #include <iostream>
Chris@1247: #include <stdexcept>
Chris@679: 
Chris@1480: #include "system/System.h"
Chris@1480: 
Chris@679: /**
Chris@679:    Resource files may be found in three places:
Chris@679: 
Chris@679:    * Bundled into the application as Qt4 resources.  These may be
Chris@679:      opened using Qt classes such as QFile, with "fake" file paths
Chris@679:      starting with a colon.  For example ":icons/fileopen.png".
Chris@679: 
Chris@679:    * Installed with the package, or in the user's equivalent home
Chris@679:      directory location.  For example,
Chris@679: 
Chris@679:      - on Linux, in /usr/share/<appname> or /usr/local/share/<appname>
Chris@679:      - on Linux, in $HOME/.local/share/<appname>
Chris@679: 
Chris@679:      - on OS/X, in /Library/Application Support/<appname>
Chris@679:      - on OS/X, in $HOME/Library/Application Support/<appname>
Chris@679: 
Chris@679:      - on Windows, in %ProgramFiles%/<company>/<appname>
Chris@680:      - on Windows, in (where?) something from http://msdn.microsoft.com/en-us/library/dd378457%28v=vs.85%29.aspx ?
Chris@679: 
Chris@679:    These locations are searched in reverse order (user-installed
Chris@679:    copies take priority over system-installed copies take priority
Chris@679:    over bundled copies).  Also, /usr/local takes priority over /usr.
Chris@679: */
Chris@679: 
Chris@679: QStringList
Chris@679: ResourceFinder::getSystemResourcePrefixList()
Chris@679: {
Chris@679:     // returned in order of priority
Chris@679: 
Chris@679:     QStringList list;
Chris@679: 
Chris@679: #ifdef Q_OS_WIN32
Chris@1480:     std::string programFiles;
Chris@1480:     (void)getEnvUtf8("ProgramFiles", programFiles);
Chris@1480:     if (programFiles != "") {
Chris@679:         list << QString("%1/%2/%3")
Chris@1480:             .arg(QString::fromStdString(programFiles))
Chris@679:             .arg(qApp->organizationName())
Chris@679:             .arg(qApp->applicationName());
Chris@679:     } else {
Chris@679:         list << QString("C:/Program Files/%1/%2")
Chris@679:             .arg(qApp->organizationName())
Chris@679:             .arg(qApp->applicationName());
Chris@679:     }
Chris@679: #else
Chris@679: #ifdef Q_OS_MAC
Chris@733:     list << QString("/Library/Application Support/%1")
Chris@679:         .arg(qApp->applicationName());
Chris@679: #else
Chris@679:     list << QString("/usr/local/share/%1")
Chris@679:         .arg(qApp->applicationName());
Chris@679:     list << QString("/usr/share/%1")
Chris@679:         .arg(qApp->applicationName());
Chris@679: #endif
Chris@679: #endif    
Chris@679: 
Chris@679:     return list;
Chris@679: }
Chris@679: 
Chris@983: static QString
Chris@983: getOldStyleUserResourcePrefix()
Chris@679: {
Chris@680: #ifdef Q_OS_WIN32
Chris@983:     // This is awkward and does not work correctly for non-ASCII home
Chris@983:     // directory names, hence getNewStyleUserResourcePrefix() below
Chris@680:     char *homedrive = getenv("HOMEDRIVE");
Chris@680:     char *homepath = getenv("HOMEPATH");
Chris@680:     QString home;
Chris@680:     if (homedrive && homepath) {
Chris@680:         home = QString("%1%2").arg(homedrive).arg(homepath);
Chris@680:     } else {
Chris@680:         home = QDir::home().absolutePath();
Chris@680:     }
Chris@680:     if (home == "") return "";
Chris@708:     return QString("%1/.%2").arg(home).arg(qApp->applicationName()); //!!! wrong
Chris@680: #else
Chris@679:     char *home = getenv("HOME");
Chris@679:     if (!home || !home[0]) return "";
Chris@679: #ifdef Q_OS_MAC
Chris@733:     return QString("%1/Library/Application Support/%2")
Chris@679:         .arg(home)
Chris@679:         .arg(qApp->applicationName());
Chris@679: #else
Chris@679:     return QString("%1/.local/share/%2")
Chris@679:         .arg(home)
Chris@679:         .arg(qApp->applicationName());
Chris@679: #endif
Chris@982: #endif
Chris@983: }
Chris@983: 
Chris@983: static QString
Chris@983: getNewStyleUserResourcePrefix()
Chris@983: {
Chris@1772:     if (qApp->applicationName() == "" || qApp->organizationName() == "") {
Chris@1772:         cerr << "ERROR: Can't use ResourceFinder before setting application and organization name" << endl;
Chris@1772:         throw std::logic_error("Can't use ResourceFinder before setting application and organization name");
Chris@1247:     }
Chris@1772:     
Chris@983: #if QT_VERSION >= 0x050000
Chris@1398: 
Chris@983:     // This is expected to be much more reliable than
Chris@983:     // getOldStyleUserResourcePrefix(), but it returns a different
Chris@983:     // directory because it includes the organisation name (which is
Chris@983:     // fair enough). Hence migrateOldStyleResources() which moves
Chris@983:     // across any resources found in the old-style path the first time
Chris@983:     // we look for the new-style one
Chris@1398: #if QT_VERSION >= 0x050400
Chris@1247:     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
Chris@983: #else
Chris@1398:     cerr << "WARNING: ResourceFinder::getOldStyleUserResourcePrefix: Building with older version of Qt (pre 5.4), resource location may be incompatible with future versions" << endl;
Chris@1398:     return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
Chris@1398: #endif
Chris@1398:     
Chris@1398: #else
Chris@1398:     cerr << "WARNING: ResourceFinder::getOldStyleUserResourcePrefix: Building with very old version of Qt (pre 5.0?), resource location may be incompatible with future versions" << endl;
Chris@983:     return getOldStyleUserResourcePrefix();
Chris@982: #endif
Chris@679: }
Chris@679: 
Chris@983: static void
Chris@983: migrateOldStyleResources()
Chris@983: {
Chris@983:     QString oldPath = getOldStyleUserResourcePrefix();
Chris@983:     QString newPath = getNewStyleUserResourcePrefix();
Chris@983:     
Chris@983:     if (oldPath != newPath &&
Chris@983:         QDir(oldPath).exists() &&
Chris@983:         !QDir(newPath).exists()) {
Chris@983: 
Chris@983:         QDir d(oldPath);
Chris@983:         
Chris@983:         if (!d.mkpath(newPath)) {
Chris@983:             cerr << "WARNING: Failed to create new-style resource path \""
Chris@983:                  << newPath << "\" to migrate old resources to" << endl;
Chris@983:             return;
Chris@983:         }
Chris@983: 
Chris@983:         QDir target(newPath);
Chris@983: 
Chris@983:         bool success = true;
Chris@983: 
Chris@983:         QStringList entries
Chris@983:             (d.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot));
Chris@983: 
Chris@983:         foreach (QString entry, entries) {
Chris@983:             if (d.rename(entry, target.filePath(entry))) {
Chris@983:                 cerr << "NOTE: Successfully moved resource \""
Chris@983:                      << entry << "\" from old resource path to new" << endl;
Chris@983:             } else {
Chris@983:                 cerr << "WARNING: Failed to move old resource \""
Chris@983:                      << entry << "\" from old location \""
Chris@983:                      << oldPath << "\" to new location \""
Chris@983:                      << newPath << "\"" << endl;
Chris@983:                 success = false;
Chris@983:             }
Chris@983:         }
Chris@983: 
Chris@983:         if (success) {
Chris@983:             if (!d.rmdir(oldPath)) {
Chris@983:                 cerr << "WARNING: Failed to remove old resource path \""
Chris@983:                      << oldPath << "\" after migrating " << entries.size()
Chris@983:                      << " resource(s) to new path \"" << newPath
Chris@983:                      << "\" (directory not empty?)" << endl;
Chris@983:             } else {
Chris@983:                 cerr << "NOTE: Successfully moved " << entries.size()
Chris@983:                      << " resource(s) from old resource "
Chris@983:                      << "path \"" << oldPath << "\" to new path \""
Chris@983:                      << newPath << "\"" << endl;
Chris@983:             }
Chris@983:         }
Chris@983:     }
Chris@983: }
Chris@983: 
Chris@983: QString
Chris@983: ResourceFinder::getUserResourcePrefix()
Chris@983: {
Chris@983:     migrateOldStyleResources();
Chris@983:     return getNewStyleUserResourcePrefix();
Chris@983: }
Chris@983: 
Chris@679: QStringList
Chris@679: ResourceFinder::getResourcePrefixList()
Chris@679: {
Chris@679:     // returned in order of priority
Chris@679: 
Chris@679:     QStringList list;
Chris@679: 
Chris@679:     QString user = getUserResourcePrefix();
Chris@679:     if (user != "") list << user;
Chris@679: 
Chris@679:     list << getSystemResourcePrefixList();
Chris@679: 
Chris@679:     list << ":"; // bundled resource location
Chris@679: 
Chris@679:     return list;
Chris@679: }
Chris@679: 
Chris@679: QString
Chris@679: ResourceFinder::getResourcePath(QString resourceCat, QString fileName)
Chris@679: {
Chris@679:     // We don't simply call getResourceDir here, because that returns
Chris@679:     // only the "installed file" location.  We also want to search the
Chris@679:     // bundled resources and user-saved files.
Chris@679: 
Chris@679:     QStringList prefixes = getResourcePrefixList();
Chris@679:     
Chris@679:     if (resourceCat != "") resourceCat = "/" + resourceCat;
Chris@679: 
Chris@679:     for (QStringList::const_iterator i = prefixes.begin();
Chris@679:          i != prefixes.end(); ++i) {
Chris@679:         
Chris@679:         QString prefix = *i;
Chris@679: 
Chris@1398: //        cerr << "ResourceFinder::getResourcePath: Looking up file \"" << fileName << "\" for category \"" << resourceCat << "\" in prefix \"" << prefix << "\"" << endl;
Chris@679: 
Chris@679:         QString path =
Chris@679:             QString("%1%2/%3").arg(prefix).arg(resourceCat).arg(fileName);
Chris@679:         if (QFileInfo(path).exists() && QFileInfo(path).isReadable()) {
Chris@1398: //            cerr << "Found it!" << endl;
Chris@679:             return path;
Chris@679:         }
Chris@679:     }
Chris@679: 
Chris@679:     return "";
Chris@679: }
Chris@679: 
Chris@679: QString
Chris@679: ResourceFinder::getResourceDir(QString resourceCat)
Chris@679: {
Chris@679:     // Returns only the "installed file" location
Chris@679: 
Chris@679:     QStringList prefixes = getSystemResourcePrefixList();
Chris@679:     
Chris@679:     if (resourceCat != "") resourceCat = "/" + resourceCat;
Chris@679: 
Chris@679:     for (QStringList::const_iterator i = prefixes.begin();
Chris@679:          i != prefixes.end(); ++i) {
Chris@679:         
Chris@679:         QString prefix = *i;
Chris@679:         QString path = QString("%1%2").arg(prefix).arg(resourceCat);
Chris@679:         if (QFileInfo(path).exists() &&
Chris@679:             QFileInfo(path).isDir() &&
Chris@679:             QFileInfo(path).isReadable()) {
Chris@679:             return path;
Chris@679:         }
Chris@679:     }
Chris@679: 
Chris@679:     return "";
Chris@679: }
Chris@679: 
Chris@679: QString
Chris@679: ResourceFinder::getResourceSavePath(QString resourceCat, QString fileName)
Chris@679: {
Chris@679:     QString dir = getResourceSaveDir(resourceCat);
Chris@679:     if (dir == "") return "";
Chris@679: 
Chris@679:     return dir + "/" + fileName;
Chris@679: }
Chris@679: 
Chris@679: QString
Chris@679: ResourceFinder::getResourceSaveDir(QString resourceCat)
Chris@679: {
Chris@679:     // Returns the "user" location
Chris@679: 
Chris@679:     QString user = getUserResourcePrefix();
Chris@679:     if (user == "") return "";
Chris@679: 
Chris@679:     if (resourceCat != "") resourceCat = "/" + resourceCat;
Chris@679: 
Chris@679:     QDir userDir(user);
Chris@679:     if (!userDir.exists()) {
Chris@679:         if (!userDir.mkpath(user)) {
Chris@843:             cerr << "ResourceFinder::getResourceSaveDir: ERROR: Failed to create user resource path \"" << user << "\"" << endl;
Chris@679:             return "";
Chris@679:         }
Chris@679:     }
Chris@679: 
Chris@679:     if (resourceCat != "") {
Chris@679:         QString save = QString("%1%2").arg(user).arg(resourceCat);
Chris@679:         QDir saveDir(save);
Chris@679:         if (!saveDir.exists()) {
Chris@959:             if (!saveDir.mkpath(save)) {
Chris@843:                 cerr << "ResourceFinder::getResourceSaveDir: ERROR: Failed to create user resource path \"" << save << "\"" << endl;
Chris@679:                 return "";
Chris@679:             }
Chris@679:         }
Chris@679:         return save;
Chris@679:     } else {
Chris@679:         return user;
Chris@679:     }
Chris@679: }
Chris@679: 
Chris@679: QStringList
Chris@679: ResourceFinder::getResourceFiles(QString resourceCat, QString fileExt)
Chris@679: {
Chris@679:     QStringList results;
Chris@679:     QStringList prefixes = getResourcePrefixList();
Chris@679: 
Chris@679:     QStringList filters;
Chris@679:     filters << QString("*.%1").arg(fileExt);
Chris@679: 
Chris@679:     for (QStringList::const_iterator i = prefixes.begin();
Chris@679:          i != prefixes.end(); ++i) {
Chris@679:         
Chris@679:         QString prefix = *i;
Chris@679:         QString path;
Chris@679: 
Chris@679:         if (resourceCat != "") {
Chris@679:             path = QString("%1/%2").arg(prefix).arg(resourceCat);
Chris@679:         } else {
Chris@679:             path = prefix;
Chris@679:         }
Chris@679:         
Chris@679:         QDir dir(path);
Chris@679:         if (!dir.exists()) continue;
Chris@679: 
Chris@679:         dir.setNameFilters(filters);
Chris@679:         QStringList entries = dir.entryList
Chris@679:             (QDir::Files | QDir::Readable, QDir::Name);
Chris@679:         
Chris@679:         for (QStringList::const_iterator j = entries.begin();
Chris@679:              j != entries.end(); ++j) {
Chris@679:             results << QString("%1/%2").arg(path).arg(*j);
Chris@679:         }
Chris@679:     }
Chris@679: 
Chris@679:     return results;
Chris@679: }
Chris@679: 
Chris@679: bool
Chris@679: ResourceFinder::unbundleResource(QString resourceCat, QString fileName)
Chris@679: {
Chris@679:     QString path = getResourcePath(resourceCat, fileName);
Chris@679:     
Chris@679:     if (!path.startsWith(':')) return true;
Chris@679: 
Chris@679:     // This is the lowest-priority alternative path for this
Chris@679:     // resource, so we know that there must be no installed copy.
Chris@679:     // Install one to the user location.
Chris@690:     SVDEBUG << "ResourceFinder::unbundleResource: File " << fileName << " is bundled, un-bundling it" << endl;
Chris@679:     QString target = getResourceSavePath(resourceCat, fileName);
Chris@679:     QFile file(path);
Chris@679:     if (!file.copy(target)) {
Chris@843:         cerr << "ResourceFinder::unbundleResource: ERROR: Failed to un-bundle resource file \"" << fileName << "\" to user location \"" << target << "\"" << endl;
Chris@679:         return false;
Chris@679:     }
Chris@679: 
Chris@679:     QFile chmod(target);
Chris@679:     chmod.setPermissions(QFile::ReadOwner |
Chris@679:                          QFile::ReadUser  | /* for potential platform-independence */
Chris@679:                          QFile::ReadGroup |
Chris@679:                          QFile::ReadOther |
Chris@679:                          QFile::WriteOwner|
Chris@679:                          QFile::WriteUser); /* for potential platform-independence */
Chris@679: 
Chris@679:     return true;
Chris@679: }
Chris@679: