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