/ src / Ryujinx.Common / Configuration / AppDataManager.cs
AppDataManager.cs
  1  using Ryujinx.Common.Logging;
  2  using Ryujinx.Common.Utilities;
  3  using System;
  4  using System.IO;
  5  using System.Runtime.Versioning;
  6  
  7  namespace Ryujinx.Common.Configuration
  8  {
  9      public static class AppDataManager
 10      {
 11          private const string DefaultBaseDir = "Ryujinx";
 12          private const string DefaultPortableDir = "portable";
 13  
 14          // The following 3 are always part of Base Directory
 15          private const string GamesDir = "games";
 16          private const string ProfilesDir = "profiles";
 17          private const string KeysDir = "system";
 18  
 19          public enum LaunchMode
 20          {
 21              UserProfile,
 22              Portable,
 23              Custom,
 24          }
 25  
 26          public static LaunchMode Mode { get; private set; }
 27  
 28          public static string BaseDirPath { get; private set; }
 29          public static string GamesDirPath { get; private set; }
 30          public static string ProfilesDirPath { get; private set; }
 31          public static string KeysDirPath { get; private set; }
 32          public static string KeysDirPathUser { get; }
 33  
 34          public static string LogsDirPath { get; private set; }
 35  
 36          public const string DefaultNandDir = "bis";
 37          public const string DefaultSdcardDir = "sdcard";
 38          private const string DefaultModsDir = "mods";
 39  
 40          public static string CustomModsPath { get; set; }
 41          public static string CustomSdModsPath { get; set; }
 42          public static string CustomNandPath { get; set; } // TODO: Actually implement this into VFS
 43          public static string CustomSdCardPath { get; set; } // TODO: Actually implement this into VFS
 44  
 45          static AppDataManager()
 46          {
 47              KeysDirPathUser = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch");
 48          }
 49  
 50          public static void Initialize(string baseDirPath)
 51          {
 52              string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
 53  
 54              if (appDataPath.Length == 0)
 55              {
 56                  appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 57              }
 58  
 59              string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
 60              string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
 61  
 62              // On macOS, check for a portable directory next to the app bundle as well.
 63              if (OperatingSystem.IsMacOS() && !Directory.Exists(portablePath))
 64              {
 65                  string bundlePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", ".."));
 66                  // Make sure we're actually running within an app bundle.
 67                  if (bundlePath.EndsWith(".app"))
 68                  {
 69                      portablePath = Path.GetFullPath(Path.Combine(bundlePath, "..", DefaultPortableDir));
 70                  }
 71              }
 72  
 73              if (Directory.Exists(portablePath))
 74              {
 75                  BaseDirPath = portablePath;
 76                  Mode = LaunchMode.Portable;
 77              }
 78              else
 79              {
 80                  BaseDirPath = userProfilePath;
 81                  Mode = LaunchMode.UserProfile;
 82              }
 83  
 84              if (baseDirPath != null && baseDirPath != userProfilePath)
 85              {
 86                  if (!Directory.Exists(baseDirPath))
 87                  {
 88                      Logger.Error?.Print(LogClass.Application, $"Custom Data Directory '{baseDirPath}' does not exist. Falling back to {Mode}...");
 89                  }
 90                  else
 91                  {
 92                      BaseDirPath = baseDirPath;
 93                      Mode = LaunchMode.Custom;
 94                  }
 95              }
 96  
 97              BaseDirPath = Path.GetFullPath(BaseDirPath); // convert relative paths
 98  
 99              if (IsPathSymlink(BaseDirPath))
100              {
101                  Logger.Warning?.Print(LogClass.Application, $"Application data directory is a symlink. This may be unintended.");
102              }
103  
104              SetupBasePaths();
105          }
106  
107          public static string GetOrCreateLogsDir()
108          {
109              if (Directory.Exists(LogsDirPath))
110              {
111                  return LogsDirPath;
112              }
113  
114              Logger.Notice.Print(LogClass.Application, "Logging directory not found; attempting to create new logging directory.");
115              LogsDirPath = SetUpLogsDir();
116  
117              return LogsDirPath;
118          }
119  
120          private static string SetUpLogsDir()
121          {
122              string logDir = "";
123  
124              if (Mode == LaunchMode.Portable)
125              {
126                  logDir = Path.Combine(BaseDirPath, "Logs");
127                  try
128                  {
129                      Directory.CreateDirectory(logDir);
130                  }
131                  catch
132                  {
133                      Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
134  
135                      return null;
136                  }
137              }
138              else
139              {
140                  if (OperatingSystem.IsMacOS())
141                  {
142                      // NOTE: Should evaluate to "~/Library/Logs/Ryujinx/".
143                      logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Logs", DefaultBaseDir);
144                      try
145                      {
146                          Directory.CreateDirectory(logDir);
147                      }
148                      catch
149                      {
150                          Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
151                          logDir = "";
152                      }
153  
154                      if (string.IsNullOrEmpty(logDir))
155                      {
156                          // NOTE: Should evaluate to "~/Library/Application Support/Ryujinx/Logs".
157                          logDir = Path.Combine(BaseDirPath, "Logs");
158  
159                          try
160                          {
161                              Directory.CreateDirectory(logDir);
162                          }
163                          catch
164                          {
165                              Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
166  
167                              return null;
168                          }
169                      }
170                  }
171                  else if (OperatingSystem.IsWindows())
172                  {
173                      // NOTE: Should evaluate to a "Logs" directory in whatever directory Ryujinx was launched from.
174                      logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
175                      try
176                      {
177                          Directory.CreateDirectory(logDir);
178                      }
179                      catch
180                      {
181                          Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
182                          logDir = "";
183                      }
184  
185                      if (string.IsNullOrEmpty(logDir))
186                      {
187                          // NOTE: Should evaluate to "C:\Users\user\AppData\Roaming\Ryujinx\Logs".
188                          logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir, "Logs");
189  
190                          try
191                          {
192                              Directory.CreateDirectory(logDir);
193                          }
194                          catch
195                          {
196                              Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
197  
198                              return null;
199                          }
200                      }
201                  }
202                  else if (OperatingSystem.IsLinux())
203                  {
204                      // NOTE: Should evaluate to "~/.config/Ryujinx/Logs".
205                      logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir, "Logs");
206  
207                      try
208                      {
209                          Directory.CreateDirectory(logDir);
210                      }
211                      catch
212                      {
213                          Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
214  
215                          return null;
216                      }
217                  }
218              }
219  
220              return logDir;
221          }
222  
223          private static void SetupBasePaths()
224          {
225              Directory.CreateDirectory(BaseDirPath);
226              LogsDirPath = SetUpLogsDir();
227              Directory.CreateDirectory(GamesDirPath = Path.Combine(BaseDirPath, GamesDir));
228              Directory.CreateDirectory(ProfilesDirPath = Path.Combine(BaseDirPath, ProfilesDir));
229              Directory.CreateDirectory(KeysDirPath = Path.Combine(BaseDirPath, KeysDir));
230          }
231  
232          // Check if existing old baseDirPath is a symlink, to prevent possible errors.
233          // Should be removed, when the existence of the old directory isn't checked anymore.
234          private static bool IsPathSymlink(string path)
235          {
236              try
237              {
238                  FileAttributes attributes = File.GetAttributes(path);
239                  return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
240              }
241              catch
242              {
243                  return false;
244              }
245          }
246  
247          [SupportedOSPlatform("macos")]
248          public static void FixMacOSConfigurationFolders()
249          {
250              string oldConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
251                  ".config", DefaultBaseDir);
252              if (Path.Exists(oldConfigPath) && !IsPathSymlink(oldConfigPath) && !Path.Exists(BaseDirPath))
253              {
254                  FileSystemUtils.MoveDirectory(oldConfigPath, BaseDirPath);
255                  Directory.CreateSymbolicLink(oldConfigPath, BaseDirPath);
256              }
257  
258              string correctApplicationDataDirectoryPath =
259                  Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
260              if (IsPathSymlink(correctApplicationDataDirectoryPath))
261              {
262                  //copy the files somewhere temporarily
263                  string tempPath = Path.Combine(Path.GetTempPath(), DefaultBaseDir);
264                  try
265                  {
266                      FileSystemUtils.CopyDirectory(correctApplicationDataDirectoryPath, tempPath, true);
267                  }
268                  catch (Exception exception)
269                  {
270                      Logger.Error?.Print(LogClass.Application,
271                          $"Critical error copying Ryujinx application data into the temp folder. {exception}");
272                      try
273                      {
274                          FileSystemInfo resolvedDirectoryInfo =
275                              Directory.ResolveLinkTarget(correctApplicationDataDirectoryPath, true);
276                          string resolvedPath = resolvedDirectoryInfo.FullName;
277                          Logger.Error?.Print(LogClass.Application, $"Please manually move your Ryujinx data from {resolvedPath} to {correctApplicationDataDirectoryPath}, and remove the symlink.");
278                      }
279                      catch (Exception symlinkException)
280                      {
281                          Logger.Error?.Print(LogClass.Application, $"Unable to resolve the symlink for Ryujinx application data: {symlinkException}. Follow the symlink at {correctApplicationDataDirectoryPath} and move your data back to the Application Support folder.");
282                      }
283                      return;
284                  }
285  
286                  //delete the symlink
287                  try
288                  {
289                      //This will fail if this is an actual directory, so there is no way we can actually delete user data here.
290                      File.Delete(correctApplicationDataDirectoryPath);
291                  }
292                  catch (Exception exception)
293                  {
294                      Logger.Error?.Print(LogClass.Application,
295                          $"Critical error deleting the Ryujinx application data folder symlink at {correctApplicationDataDirectoryPath}. {exception}");
296                      try
297                      {
298                          FileSystemInfo resolvedDirectoryInfo =
299                              Directory.ResolveLinkTarget(correctApplicationDataDirectoryPath, true);
300                          string resolvedPath = resolvedDirectoryInfo.FullName;
301                          Logger.Error?.Print(LogClass.Application, $"Please manually move your Ryujinx data from {resolvedPath} to {correctApplicationDataDirectoryPath}, and remove the symlink.");
302                      }
303                      catch (Exception symlinkException)
304                      {
305                          Logger.Error?.Print(LogClass.Application, $"Unable to resolve the symlink for Ryujinx application data: {symlinkException}. Follow the symlink at {correctApplicationDataDirectoryPath} and move your data back to the Application Support folder.");
306                      }
307                      return;
308                  }
309  
310                  //put the files back
311                  try
312                  {
313                      FileSystemUtils.CopyDirectory(tempPath, correctApplicationDataDirectoryPath, true);
314                  }
315                  catch (Exception exception)
316                  {
317                      Logger.Error?.Print(LogClass.Application,
318                          $"Critical error copying Ryujinx application data into the correct location. {exception}. Please manually move your application data from {tempPath} to {correctApplicationDataDirectoryPath}.");
319                  }
320              }
321          }
322  
323          public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName;
324          public static string GetSdModsPath() => CustomSdModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName;
325      }
326  }