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 static QList<DirErrno> uniqueErrnosByDir(const QList<DirErrno> &errnos)
49 {
50  QSet<QString> uniqueDirs;
51  QList<DirErrno> uniqueErrnos;
52  for (const DirErrno &dirErrno : errnos)
53  {
54  if (!uniqueDirs.contains(dirErrno.directory.path()))
55  {
56  uniqueDirs.insert(dirErrno.directory.path());
57  uniqueErrnos << dirErrno;
58  }
59  }
60  return uniqueErrnos;
61 }
62 
63 static QStringList uniquePaths(const QStringList &paths)
64 {
65  QList<QFileInfo> uniqueMarkers;
66  QStringList result;
67  for (const QString &path : paths)
68  {
69  QFileInfo fileInfo(path);
70  if (!uniqueMarkers.contains(fileInfo))
71  {
72  uniqueMarkers << fileInfo;
73  result << path;
74  }
75  }
76  return result;
77 }
78 
79 DClass<DataPaths>
80 {
81 public:
82  static const QString PLUGINS_DIR_NAME;
83 
84  QDir cacheDirectory;
85  QDir configDirectory;
86  QDir dataDirectory;
87 
88  bool bIsPortableModeOn;
89 
90  QDir portableRoot() const
91  {
92  return QDir::current();
93  }
94 
95  QString legacyAppDataBaseDir() const
96  {
97  if (bIsPortableModeOn)
98  {
99  return QCoreApplication::applicationDirPath();
100  }
101 
102  // For non-portable model this continues here:
103  QString dir;
104 
105 #ifdef Q_OS_WIN32
106  // Let's open new block to prevent variable "bleeding".
107  {
108  QString envVar = QProcessEnvironment::systemEnvironment().value("APPDATA");
109  if (validateDir(envVar))
110  dir = envVar;
111  }
112 #endif
113 
114  if (dir.isEmpty())
115  {
116  dir = QDir::homePath();
117  if (!validateDir(dir))
118  return QString();
119  }
120  return dir;
121  }
122 
123  bool validateDir(const QString &path) const
124  {
125  QFileInfo fileInfo(path);
126  return !path.isEmpty() && fileInfo.exists() && fileInfo.isDir();
127  }
128 };
129 
130 DPointered(DataPaths)
131 
132 DataPaths *DataPaths::staticDefaultInstance = nullptr;
133 
134 static const QString CACHE_DIR_NAME = ".cache";
135 static const QString CONFIG_DIR_NAME = ".doomseeker";
136 static const QString DATA_DIR_NAME = ".static";
137 static const QString DEMOS_DIR_NAME = "demos";
138 
139 const QString DataPaths::CHATLOGS_DIR_NAME = "chatlogs";
140 const QString PrivData<DataPaths>::PLUGINS_DIR_NAME = "plugins";
141 const QString DataPaths::TRANSLATIONS_DIR_NAME = "translations";
142 const QString DataPaths::UPDATE_PACKAGES_DIR_NAME = "updates";
143 const QString DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX = "doomseeker-update-pkg-";
144 
145 DataPaths::DataPaths(bool bPortableModeOn)
146 {
147  d->bIsPortableModeOn = bPortableModeOn;
148 
149  if (bPortableModeOn)
150  {
151  setBaseDir(QDir("."));
152  }
153  else
154  {
155  d->cacheDirectory.setPath(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
156  #if QT_VERSION >= 0x050500
157  // QStandardPaths::AppConfigLocation was added in Qt 5.5.
158  d->configDirectory.setPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
159  #else
160  // In older 5.x versions we need to construct the config path ourselves.
161  d->configDirectory.setPath(Strings::combinePaths(
162  QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation),
164  #endif
165  d->dataDirectory.setPath(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
166  }
167 }
168 
169 DataPaths::~DataPaths()
170 {
171 }
172 
174 {
175  return d->cacheDirectory.path();
176 }
177 
178 QStringList DataPaths::canWrite() const
179 {
180  QStringList failedList;
181 
182  QString dataDirectory = programsDataDirectoryPath();
183  if (!d->validateDir(dataDirectory))
184  failedList.append(dataDirectory);
185 
186  return failedList;
187 }
188 
189 QList<DirErrno> DataPaths::createDirectories()
190 {
191  QList<DirErrno> failedDirs;
192 
193  DirErrno cacheDirError = FileUtils::mkpath(d->cacheDirectory);
194  if (cacheDirError.isError())
195  failedDirs << cacheDirError;
196 
197  DirErrno configDirError = FileUtils::mkpath(d->configDirectory);
198  if (configDirError.isError())
199  failedDirs << configDirError;
200 
201  DirErrno dataDirError = FileUtils::mkpath(d->dataDirectory);
202  if (dataDirError.isError())
203  failedDirs << dataDirError;
204 
205  DirErrno demosDirError = FileUtils::mkpath(d->dataDirectory.filePath(DEMOS_DIR_NAME));
206  if (demosDirError.isError())
207  failedDirs << demosDirError;
208 
209  return uniqueErrnosByDir(failedDirs);
210 }
211 
213 {
214  return staticDefaultInstance;
215 }
216 
217 
218 QStringList DataPaths::defaultWadPaths() const
219 {
220  QStringList filePaths;
221 
222  if (d->bIsPortableModeOn)
223  {
224  filePaths << localDataLocationPath();
225  filePaths << programsDataDirectoryPath();
226  filePaths << ".";
227  for (int i = 0; i < filePaths.size(); ++i)
228  filePaths[i] = portablizePath(filePaths[i]);
229  }
230  else
231  {
232  filePaths << localDataLocationPath();
233  filePaths << programsDataDirectoryPath();
234  // The directory which contains the Doomseeker executable may be a good
235  // default choice, but on unix systems the bin directory is not worth
236  // searching. Similarly for Mac application bundle.
237  const QDir appDir(QCoreApplication::applicationDirPath());
238  const QString progBinDirName = appDir.dirName();
239  if (progBinDirName != "bin" && progBinDirName != "MacOS")
240  filePaths << appDir.path();
241  }
242 
243  return filePaths;
244 }
245 
247 {
248  return d->dataDirectory.filePath(DEMOS_DIR_NAME);
249 }
250 
251 QString DataPaths::documentsLocationPath(const QString &subpath) const
252 {
253  QString rootPath;
254  if (!isPortableModeOn())
255  {
256  rootPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
257  rootPath = Strings::combinePaths(rootPath, QCoreApplication::applicationName());
258  }
259  else
260  {
261  rootPath = QCoreApplication::applicationDirPath();
262  rootPath = Strings::combinePaths(rootPath, "storage");
263  }
264  return QDir(rootPath).filePath(subpath);
265 }
266 
267 QString DataPaths::env(const QString &key)
268 {
269  return QProcessEnvironment::systemEnvironment().value(key);
270 }
271 
272 void DataPaths::initDefault(bool bPortableModeOn)
273 {
274  assert(staticDefaultInstance == nullptr && "DataPaths can have only one default.");
275  if (staticDefaultInstance == nullptr)
276  staticDefaultInstance = new DataPaths(bPortableModeOn);
277 }
278 
279 bool DataPaths::isPortableModeOn() const
280 {
281  return d->bIsPortableModeOn;
282 }
283 
284 QString DataPaths::localDataLocationPath(const QString &subpath) const
285 {
286  return Strings::combinePaths(d->dataDirectory.path(), subpath);
287 }
288 
290 {
291  return localDataLocationPath(QString("%1/%2").arg(
293 }
294 
296 {
297  return documentsLocationPath(QString("%1/%2").arg(
299 }
300 
301 QStringList DataPaths::pluginSearchLocationPaths() const
302 {
303  QStringList paths;
304  paths.append(QCoreApplication::applicationDirPath());
305  paths.append("./");
306 
307  #if !defined(Q_OS_DARWIN) && !defined(Q_OS_WIN32)
308  // On systems where we install to a fixed location, if we see that we are
309  // running an installed binary, then we should only load plugins from the
310  // expected location. Otherwise use it only as a last resort.
311  const QString installDir = INSTALL_PREFIX "/" INSTALL_LIBDIR "/doomseeker/";
312  if (QCoreApplication::applicationDirPath() == INSTALL_PREFIX "/bin")
313  paths = QStringList(installDir);
314  else
315  paths.append(installDir);
316  #endif
317 
318  paths = uniquePaths(paths);
319  return Strings::combineManyPaths(paths, "engines/");
320 }
321 
322 QString DataPaths::portablizePath(const QString &path) const
323 {
324  if (isPortableModeOn())
325  {
326  QFileInfo pathInfo(path);
327  if (pathInfo.isAbsolute())
328  {
329  return d->portableRoot().relativeFilePath(path);
330  }
331  }
332  return path;
333 }
334 
335 QString DataPaths::programFilesDirectory(MachineType machineType)
336 {
337  #ifdef Q_OS_WIN32
338  QString envVarName = "";
339 
340  switch (machineType)
341  {
342  case x86:
343  envVarName = "ProgramFiles(x86)";
344  break;
345 
346  case x64:
347  envVarName = "ProgramW6432";
348  break;
349 
350  case Preferred:
351  envVarName = "ProgramFiles";
352  break;
353 
354  default:
355  return QString();
356  }
357 
358  QString path = env(envVarName);
359  if (path.isEmpty() && machineType != Preferred)
360  {
361  // Empty outcome may happen on 32-bit systems where variables
362  // like "ProgramFiles(x86)" may not exist.
363  //
364  // If "ProgramFiles" variable is empty then something is seriously
365  // wrong with the system.
366  path = programFilesDirectory(Preferred);
367  }
368 
369  return path;
370 
371  #else
372  Q_UNUSED(machineType);
373  return QString();
374  #endif
375 }
376 
378 {
379  return d->configDirectory.path();
380 }
381 
382 void DataPaths::setBaseDir(const QDir &baseDir)
383 {
384  d->cacheDirectory.setPath(baseDir.filePath(CACHE_DIR_NAME));
385  d->configDirectory.setPath(baseDir.filePath(CONFIG_DIR_NAME));
386  d->dataDirectory.setPath(baseDir.filePath(DATA_DIR_NAME));
387 }
388 
389 void DataPaths::setWorkingDirectory(const QString &workingDirectory)
390 {
391  // no-op; this method is deprecated
392 }
393 
394 QStringList DataPaths::staticDataSearchDirs(const QString &subdir)
395 {
396  QStringList paths;
397  paths.append(QDir::currentPath()); // current working dir
398  paths.append(QCoreApplication::applicationDirPath()); // where exe is located
399  #ifndef Q_OS_WIN32
400  paths.append(INSTALL_PREFIX "/share/doomseeker"); // standard arch independent linux path
401  #endif
402  paths = uniquePaths(paths);
403  QString subdirFiltered = subdir.trimmed();
404  if (!subdirFiltered.isEmpty())
405  {
406  for (int i = 0; i < paths.size(); ++i)
407  paths[i] = Strings::combinePaths(paths[i], subdirFiltered);
408  }
409  return paths;
410 }
411 
412 QString DataPaths::systemAppDataDirectory(QString append) const
413 {
414  return QDir(d->legacyAppDataBaseDir()).filePath(append);
415 }
416 
417 bool DataPaths::validateAppDataDirectory()
418 {
419  return true;
420 }
421 
422 bool DataPaths::validateDir(const QString &path)
423 {
424  QFileInfo fileInfo(path);
425 
426  bool bCondition1 = !path.isEmpty();
427  bool bCondition2 = fileInfo.exists();
428  bool bCondition3 = fileInfo.isDir();
429 
430  return bCondition1 && bCondition2 && bCondition3;
431 }
432 
433 const QString &DataPaths::workingDirectory() const
434 {
435  // Need to use a static variable because the method returns a reference.
436  static QString dirPath = QCoreApplication::applicationDirPath();
437  return dirPath;
438 }