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 }