datapaths.cpp
1 //------------------------------------------------------------------------------
2 // datapaths.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2010 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "datapaths.h"
24 
25 #include "application.h"
26 #include "doomseekerfilepaths.h"
27 #include "fileutils.h"
28 #include "log.h"
29 #include "plugins/engineplugin.h"
30 #include "strings.hpp"
31 
32 #include <QCoreApplication>
33 #include <QDesktopServices>
34 #include <QFileInfo>
35 #include <QProcessEnvironment>
36 #include <QSet>
37 #include <cassert>
38 #include <cstdlib>
39 #include <cerrno>
40 
41 #if QT_VERSION >= 0x050000
42 #include <QStandardPaths>
43 #endif
44 
45 // Sanity check for INSTALL_PREFIX and INSTALL_LIBDIR
46 #if !defined(INSTALL_PREFIX) || !defined(INSTALL_LIBDIR)
47 #error Build system should provide definition for INSTALL_PREFIX and INSTALL_LIBDIR
48 #endif
49 
50 // On NTFS file systems, ownership and permissions checking is disabled by
51 // default for performance reasons. The following int toggles it by
52 // incrementation and decrementation of its value.
53 // See: http://doc.qt.io/qt-5/qfileinfo.html#ntfs-permissions
54 #ifdef Q_OS_WIN32
55 extern Q_CORE_EXPORT int qt_ntfs_permission_lookup;
56 #else
57 // We'll need to declare an int with the same name to compile successfully in other platforms.
58 int qt_ntfs_permission_lookup;
59 #endif
60 
61 static QList<DataPaths::DirErrno> uniqueErrnosByDir(const QList<DataPaths::DirErrno> &errnos)
62 {
63  QSet<QString> uniqueDirs;
64  QList<DataPaths::DirErrno> uniqueErrnos;
65  foreach(const DataPaths::DirErrno &dirErrno, errnos)
66  {
67  if (!uniqueDirs.contains(dirErrno.directory.path()))
68  {
69  uniqueDirs.insert(dirErrno.directory.path());
70  uniqueErrnos << dirErrno;
71  }
72  }
73  return uniqueErrnos;
74 }
75 
76 static QStringList uniquePaths(const QStringList &paths)
77 {
78  QList<QFileInfo> uniqueMarkers;
79  QStringList result;
80  foreach (const QString &path, paths)
81  {
82  if (!uniqueMarkers.contains(path))
83  {
84  uniqueMarkers << path;
85  result << path;
86  }
87  }
88  return result;
89 }
90 
91 DClass<DataPaths>
92 {
93  public:
94  static const QString PLUGINS_DIR_NAME;
95 
96  QDir cacheDirectory;
97  QDir configDirectory;
98  QDir dataDirectory;
99 
100  QString workingDirectory;
101 
102  bool bIsPortableModeOn;
103 };
104 
105 DPointered(DataPaths)
106 
107 DataPaths *DataPaths::staticDefaultInstance = NULL;
108 
109 static const QString LEGACY_APPDATA_DIR_NAME = ".doomseeker";
110 static const QString DEMOS_DIR_NAME = "demos";
111 
112 const QString DataPaths::CHATLOGS_DIR_NAME = "chatlogs";
113 const QString PrivData<DataPaths>::PLUGINS_DIR_NAME = "plugins";
114 const QString DataPaths::TRANSLATIONS_DIR_NAME = "translations";
115 const QString DataPaths::UPDATE_PACKAGES_DIR_NAME = "updates";
116 const QString DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX = "doomseeker-update-pkg-";
117 
118 DataPaths::DataPaths(bool bPortableModeOn)
119 {
120  d->bIsPortableModeOn = bPortableModeOn;
121 
122  // Logically this would be "./" but our only use of this class as of
123  // Doomseeker 1.1 would use setWorkingDirectory to applicationDirPath()
124  d->workingDirectory = QCoreApplication::applicationDirPath();
125 
126  if (bPortableModeOn)
127  {
128  d->cacheDirectory = systemAppDataDirectory(".cache");
129  d->configDirectory = systemAppDataDirectory(LEGACY_APPDATA_DIR_NAME);
130  d->dataDirectory = systemAppDataDirectory(".static");
131  }
132  else
133  {
134 #if QT_VERSION >= 0x050000
135  d->cacheDirectory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
136  #if QT_VERSION >= 0x050500
137  // QStandardPaths::AppConfigLocation was added in Qt 5.5.
138  d->configDirectory = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
139  #else
140  // In older 5.x versions we need to construct the config path ourselves.
141  d->configDirectory = Strings::combinePaths(
142  QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation),
144  #endif
145  d->dataDirectory = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
146 #else
147  d->cacheDirectory = QDesktopServices::storageLocation(QDesktopServices::CacheLocation);
148  d->dataDirectory = QDesktopServices::storageLocation(QDesktopServices::DataLocation);
149 
150 #ifdef Q_OS_MAC
151  d->configDirectory = systemAppDataDirectory("Library/Preferences/Doomseeker");
152 #else
153  d->configDirectory = systemAppDataDirectory(LEGACY_APPDATA_DIR_NAME);
154 #endif
155 
156 #endif
157  }
158 
159  gLog << QString("Cache directory: %1").arg(d->cacheDirectory.absolutePath());
160  gLog << QString("Config directory: %1").arg(d->configDirectory.absolutePath());
161  gLog << QString("Data directory: %1").arg(d->dataDirectory.absolutePath());
162 }
163 
164 DataPaths::~DataPaths()
165 {
166 }
167 
169 {
170  return d->cacheDirectory.absolutePath();
171 }
172 
173 QStringList DataPaths::canWrite() const
174 {
175  QStringList failedList;
176 
177  QString dataDirectory = programsDataDirectoryPath();
178  if (!validateDir(dataDirectory))
179  {
180  failedList.append(dataDirectory);
181  }
182 
183  return failedList;
184 }
185 
186 QList<DataPaths::DirErrno> DataPaths::createDirectories()
187 {
188  QList<DirErrno> failedDirs;
189  const QDir appDataDir(systemAppDataDirectory());
190 
191  // No need to bother with migrating plugin master caches
192  DirErrno cacheDirError = tryCreateDirectory(d->cacheDirectory, ".");
193  if (cacheDirError.isError())
194  failedDirs << cacheDirError;
195 
196  // The existential question here is needed for migration purposes,
197  // but on Windows the >=1.2 configDirectory can already exist because
198  // Doomseeker <1.2 already stored IRC chat logs there.
199  // It is necessary to ask about the .ini file.
200  if (!d->configDirectory.exists(DoomseekerFilePaths::INI_FILENAME))
201  {
202  DirErrno configDirError = tryCreateDirectory(d->configDirectory, ".");
203  if (configDirError.isError())
204  {
205  failedDirs << configDirError;
206  }
207 #if !defined(Q_OS_MAC)
208  else if (appDataDir.exists(".doomseeker"))
209  {
210  // Migrate config from old versions of Doomseeker (pre 1.2)
211  const QDir oldConfigDir(appDataDir.absolutePath() + QDir::separator() + ".doomseeker");
212  gLog << QString("Migrating configuration data from '%1'\n\tto '%2'.")
213  .arg(oldConfigDir.absolutePath())
214  .arg(d->configDirectory.absolutePath());
215 
216  foreach (QFileInfo fileinfo, oldConfigDir.entryInfoList(QStringList("*.ini"), QDir::Files))
217  QFile(fileinfo.absoluteFilePath()).copy(d->configDirectory.absoluteFilePath(fileinfo.fileName()));
218  }
219 #endif
220  }
221 
222  // In >=1.2 and on Windows platform, dataDirectory can be the same
223  // as configDirectory. To do the migration properly, we need to
224  // ask about existence of a subdirectory inside the data directory.
225  if (!d->dataDirectory.exists(DEMOS_DIR_NAME))
226  {
227 #ifdef Q_OS_MAC
228  const QString legacyPrefDirectory = "Library/Preferences/Doomseeker";
229 #else
230  const QString legacyPrefDirectory = ".doomseeker";
231 #endif
232  DirErrno dataDirError = tryCreateDirectory(d->dataDirectory, ".");
233  if (dataDirError.isError())
234  {
235  failedDirs << dataDirError;
236  }
237  else if (appDataDir.exists(legacyPrefDirectory))
238  {
239  // Migrate data from old versions of Doomseeker (specifically demos) (pre 1.2)
240  const QDir oldConfigDir(appDataDir.absolutePath() + QDir::separator() + legacyPrefDirectory);
241  gLog << QString("Migrating user data from '%1'\n\tto '%2'.")
242  .arg(oldConfigDir.absolutePath())
243  .arg(d->dataDirectory.absolutePath());
244 
245  foreach (QFileInfo fileinfo, oldConfigDir.entryInfoList(QDir::Dirs))
246  {
247  const QString origPath = fileinfo.absoluteFilePath();
248  QFile file(origPath);
249  if (file.rename(d->dataDirectory.absoluteFilePath(fileinfo.fileName())))
250  {
251  // On Windows this will create an useless .lnk shortcut
252  // without the .lnk extension, so don't bother.
253 #if !defined(Q_OS_WIN32)
254  file.link(origPath);
255 #endif
256  }
257  }
258  }
259  }
260 
261  DirErrno demosDirError = tryCreateDirectory(d->dataDirectory, DEMOS_DIR_NAME);
262  if (demosDirError.isError())
263  {
264  failedDirs << demosDirError;
265  }
266 
267  return uniqueErrnosByDir(failedDirs);
268 }
269 
271 {
272  return staticDefaultInstance;
273 }
274 
275 
276 QStringList DataPaths::defaultWadPaths() const
277 {
278  QStringList filePaths;
279  filePaths << programsDataDirectoryPath();
280 
281  // The directory which contains the Doomseeker executable may be a good
282  // default choice, but on unix systems the bin directory is not worth
283  // searching. Similarly for Mac application bundle.
284  const QString progBinDirName = QDir(workingDirectory()).dirName();
285  if(progBinDirName != "bin" && progBinDirName != "MacOS")
286  filePaths << workingDirectory();
287 
288  return filePaths;
289 }
290 
291 QString DataPaths::demosDirectoryPath() const
292 {
293  return d->dataDirectory.absoluteFilePath(DEMOS_DIR_NAME);
294 }
295 
296 QString DataPaths::documentsLocationPath(const QString &subpath) const
297 {
298  QString rootPath;
299  if (!isPortableModeOn())
300  {
301 #if QT_VERSION >= 0x050000
302  rootPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
303 #else
304  rootPath = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation);
305 #endif
306  rootPath = Strings::combinePaths(rootPath, QCoreApplication::applicationName());
307  }
308  else
309  {
310  rootPath = systemAppDataDirectory("storage");
311  }
312  return Strings::combinePaths(rootPath, subpath);
313 }
314 
315 QString DataPaths::env(const QString &key)
316 {
317  return QProcessEnvironment::systemEnvironment().value(key);
318 }
319 
320 void DataPaths::initDefault(bool bPortableModeOn)
321 {
322  assert(staticDefaultInstance == NULL && "DataPaths can have only one default.");
323  if (staticDefaultInstance == NULL)
324  {
325  staticDefaultInstance = new DataPaths(bPortableModeOn);
326  }
327 }
328 
329 bool DataPaths::isPortableModeOn() const
330 {
331  return d->bIsPortableModeOn;
332 }
333 
334 QString DataPaths::localDataLocationPath(const QString& subpath) const
335 {
336  return Strings::combinePaths(d->dataDirectory.absolutePath(), subpath);
337 }
338 
340 {
341  return localDataLocationPath(QString("%1/%2").arg(
343 }
344 
346 {
347  return documentsLocationPath(QString("%1/%2").arg(
349 }
350 
352 {
353  QStringList paths;
354  paths.append(workingDirectory());
355  paths.append("./");
356 
357 #if !defined(Q_OS_MAC) && !defined(Q_OS_WIN32)
358  // On systems where we install to a fixed location, if we see that we are
359  // running an installed binary, then we should only load plugins from the
360  // expected location. Otherwise use it only as a last resort.
361  const QString installDir = INSTALL_PREFIX "/" INSTALL_LIBDIR "/doomseeker/";
362  if(workingDirectory() == INSTALL_PREFIX "/bin")
363  paths = QStringList(installDir);
364  else
365  paths.append(installDir);
366 #endif
367 
368  paths = uniquePaths(paths);
369  return Strings::combineManyPaths(paths, "engines/");
370 }
371 
372 QString DataPaths::programFilesDirectory(MachineType machineType)
373 {
374  #ifdef Q_OS_WIN32
375  QString envVarName = "";
376 
377  switch (machineType)
378  {
379  case x86:
380  envVarName = "ProgramFiles(x86)";
381  break;
382 
383  case x64:
384  envVarName = "ProgramW6432";
385  break;
386 
387  case Preferred:
388  envVarName = "ProgramFiles";
389  break;
390 
391  default:
392  return QString();
393  }
394 
395  QString path = env(envVarName);
396  if (path.isEmpty() && machineType != Preferred)
397  {
398  // Empty outcome may happen on 32-bit systems where variables
399  // like "ProgramFiles(x86)" may not exist.
400  //
401  // If "ProgramFiles" variable is empty then something is seriously
402  // wrong with the system.
403  path = programFilesDirectory(Preferred);
404  }
405 
406  return path;
407 
408  #else
409  return QString();
410  #endif
411 }
412 
414 {
415  return d->configDirectory.absolutePath();
416 }
417 
419 {
420  d->workingDirectory = workingDirectory;
421 }
422 
423 QStringList DataPaths::staticDataSearchDirs(const QString& subdir)
424 {
425  QStringList paths;
426  paths.append(QDir::currentPath()); // current working dir
427  paths.append(QCoreApplication::applicationDirPath()); // where exe is located
428 #ifndef Q_OS_WIN32
429  paths.append(INSTALL_PREFIX "/share/doomseeker"); // standard arch independent linux path
430 #endif
431  paths = uniquePaths(paths);
432  QString subdirFiltered = subdir.trimmed();
433  if (!subdirFiltered.isEmpty())
434  {
435  for (int i = 0; i < paths.size(); ++i)
436  {
437  paths[i] = Strings::combinePaths(paths[i], subdirFiltered);
438  }
439  }
440  return paths;
441 }
442 
443 QString DataPaths::systemAppDataDirectory(QString append) const
444 {
445  Strings::triml(append, "/\\");
446 
447  if (isPortableModeOn())
448  {
449  QString path = d->workingDirectory + "/" + append;
450  return QDir(path).absolutePath();
451  }
452 
453  // For non-portable model this continues here:
454  QString dir;
455 
456  #ifdef Q_OS_WIN32
457  // Let's open new block to prevent variable "bleeding".
458  {
459  QString envVar = env("APPDATA");
460  if (validateDir(envVar))
461  {
462  dir = envVar;
463  }
464  }
465  #endif
466 
467  if (dir.isEmpty())
468  {
469  dir = QDir::homePath();
470  if (!validateDir(dir))
471  {
472  return QString();
473  }
474  }
475 
476  Strings::trimr(dir, "/\\");
477 
478  dir += QDir::separator() + append;
479 
480  return QDir(dir).absolutePath();
481 }
482 
483 DataPaths::DirErrno DataPaths::tryCreateDirectory(const QDir& rootDir, const QString& dirToCreate) const
484 {
485  QString fullDirPath = Strings::combinePaths(rootDir.path(), dirToCreate);
486 
487  // We need to reset errno to prevent false positives
488  errno = 0;
489  if (!rootDir.mkpath(dirToCreate))
490  {
491  int errnoval = errno;
492  if (errnoval != 0)
493  {
494  return DirErrno(fullDirPath, errnoval, strerror(errnoval));
495  }
496  else
497  {
498  // Try to decipher if we have permissions.
499  // First we must find the bottom-most directory that does actually exist.
500  QString pathToBottomMostExisting = FileUtils::cdUpUntilExists(
501  Strings::combinePaths(rootDir.path(), dirToCreate));
502  // If we've found a bottom-most directory that exists, we can check its permissions.
503  if (!pathToBottomMostExisting.isEmpty())
504  {
505  QFileInfo parentDir(pathToBottomMostExisting);
506  if (parentDir.exists() && !parentDir.isDir())
507  {
508  return DirErrno(fullDirPath, DirErrno::CUSTOM_ERROR,
509  QObject::tr("parent node is not a directory: %1")
510  .arg(parentDir.filePath()));
511  }
512 
513  // BLOCK - keep this to absolute mininum
514  ++qt_ntfs_permission_lookup;
515  bool permissions = parentDir.isReadable()
516  && parentDir.isWritable()
517  && parentDir.isExecutable();
518  --qt_ntfs_permission_lookup;
519  // END OF BLOCK
520  if (!permissions)
521  {
522  return DirErrno(fullDirPath, DirErrno::CUSTOM_ERROR,
523  QObject::tr("lack of necessary permissions to the parent directory: %1")
524  .arg(parentDir.filePath()));
525  }
526  }
527  // Just give up trying to deduce a correct error.
528  return DirErrno(fullDirPath, DirErrno::CUSTOM_ERROR, QObject::tr("cannot create directory"));
529  }
530  }
531  return DirErrno();
532 }
533 
535 {
537 }
538 
539 bool DataPaths::validateDir(const QString& path)
540 {
541  QFileInfo fileInfo(path);
542 
543  bool bCondition1 = !path.isEmpty();
544  bool bCondition2 = fileInfo.exists();
545  bool bCondition3 = fileInfo.isDir();
546 
547  return bCondition1 && bCondition2 && bCondition3;
548 }
549 
550 const QString &DataPaths::workingDirectory() const
551 {
552  return d->workingDirectory;
553 }
static QString combinePaths(QString pathFront, QString pathEnd)
Definition: strings.cpp:147
QStringList defaultWadPaths() const
Returns a list of the default file paths for WAD files.
Definition: datapaths.cpp:276
QString pluginDocumentsLocationPath(const EnginePlugin &plugin) const
Place where EnginePlugin can store its "My Documents" content.
Definition: datapaths.cpp:345
QList< DirErrno > createDirectories()
Creates necessary directories for application run.
Definition: datapaths.cpp:186
void setWorkingDirectory(const QString &workingDirectory)
Changes the location returned by workingDirectory.
Definition: datapaths.cpp:418
static bool validateDir(const QString &path)
Definition: datapaths.cpp:539
QString programsDataDirectoryPath() const
Path to directory where this concrete application should store its data.
Definition: datapaths.cpp:413
QString cacheLocationPath() const
Path to the cache directory with Doomseeker&#39;s own subpath suffix.
Definition: datapaths.cpp:168
Represents directories used by Doomseeker to store data.
Definition: datapaths.h:82
static QString cdUpUntilExists(QString path)
Moves upwards the path until it finds the path that exists.
Definition: fileutils.cpp:47
QStringList pluginSearchLocationPaths() const
Ordered locations were plugin libraries could be loaded from.
Definition: datapaths.cpp:351
static const QString NAME
Program name - doomseeker.
Definition: application.h:51
Struct which contains the relevant QDir, the errno reported, and the QString generated by the errno...
Definition: datapaths.h:101
Definition: dptr.h:31
static QString programFilesDirectory(MachineType machineType)
Definition: datapaths.cpp:372
QString localDataLocationPath(const QString &subpath=QString()) const
Path to the directory where large data should be saved.
Definition: datapaths.cpp:334
const QString & workingDirectory() const
Program working directory.
Definition: datapaths.cpp:550
QString documentsLocationPath(const QString &subpath=QString()) const
Path to the "My Documents" directory with Doomseeker&#39;s own subpath suffix.
Definition: datapaths.cpp:296
static QStringList combineManyPaths(const QStringList &fronts, const QString &pathEnd)
Combines path suffix with all fronts, returns new list.
Definition: strings.cpp:137
QString nameCanonical() const
Either specified explicitly by plugin or derived from the actual plugin name.
QStringList canWrite() const
Checks if all directories can be written to.
Definition: datapaths.cpp:173
QString systemAppDataDirectory(QString append=QString()) const
Gets path to the root directory for data storage.
Definition: datapaths.cpp:443
bool validateAppDataDirectory()
Checks if the root directory for Doomseeker data storage is accessible.
Definition: datapaths.cpp:534
QString pluginLocalDataLocationPath(const EnginePlugin &plugin) const
Place where EnginePlugin can store its local files.
Definition: datapaths.cpp:339
DirErrno tryCreateDirectory(const QDir &rootDir, const QString &dirToCreate) const
If directory already exists true is returned.
Definition: datapaths.cpp:483
static QStringList staticDataSearchDirs(const QString &subdir=QString())
Paths to directories where program should search for its static data.
Definition: datapaths.cpp:423
static DataPaths * defaultInstance()
Retrieves default instance that is used throughout the program.
Definition: datapaths.cpp:270