using LANCommander.SDK.Extensions; using LANCommander.SDK.Helpers; using LANCommander.SDK.Models; using LANCommander.SDK.PowerShell; using Microsoft.Extensions.Logging; using SharpCompress.Common; using SharpCompress.Readers; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using LANCommander.SDK.Utilities; // Some terms for this file since they're probably going to be needed in the future: // Local path - The full path of the file/directory on the local disk. No variables used, just the raw path for current machine // Actual path - The path where the entries should be extracted to, before expanding environemnt variables. // Archive path - The path of where the entries are located in the ZIP // // Other notes: // - Entries in the ZIP are separated by save path ID to avoid collision namespace LANCommander.SDK.Services { public class SaveService { private readonly ILogger _logger; private readonly Client _client; public delegate void OnDownloadProgressHandler(DownloadProgressChangedEventArgs e); public event OnDownloadProgressHandler OnDownloadProgress; public delegate void OnDownloadCompleteHandler(AsyncCompletedEventArgs e); public event OnDownloadCompleteHandler OnDownloadComplete; public SaveService(Client client) { _client = client; } public SaveService(Client client, ILogger logger) { _client = client; _logger = logger; } private async Task DownloadAsync(Guid id, Action progressHandler, Action completeHandler) { return await _client.DownloadRequestAsync($"/api/Saves/{id}/Download", progressHandler, completeHandler); } public async Task DownloadLatestAsync(Guid gameId, Action progressHandler, Action completeHandler) { return await _client.DownloadRequestAsync($"/api/Saves/Game/{gameId}/Latest/Download", progressHandler, completeHandler); } public IEnumerable Get(Guid gameId) { return _client.GetRequest>($"/api/Saves/Game/{gameId}"); } public async Task> GetAsync(Guid gameId) { return await _client.GetRequestAsync>($"/api/Saves/Game/{gameId}"); } public GameSave GetLatest(Guid gameId) { return _client.GetRequest($"/api/Saves/Game/{gameId}/Latest"); } public Task GetLatestAsync(Guid gameId) { return _client.GetRequestAsync($"/api/Saves/Game/{gameId}/Latest"); } public async Task DownloadAsync(string installDirectory, Guid gameId, Guid? saveId = null) { var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); string tempFile; string tempLocation = string.Empty; if (manifest != null) { string destination; if (!saveId.HasValue) { destination = await DownloadLatestAsync(manifest.Id, (changed) => { OnDownloadProgress?.Invoke(changed); }, (complete) => { OnDownloadComplete?.Invoke(complete); }); } else { destination = await DownloadAsync(saveId.Value, (changed) => { OnDownloadProgress?.Invoke(changed); }, (complete) => { OnDownloadComplete?.Invoke(complete); }); } if (string.IsNullOrWhiteSpace(destination)) return; _logger?.LogTrace("Game save archive downloaded to {SaveTempLocation}", destination); tempFile = destination; // Go into the archive and extract the files to the correct locations try { tempLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempLocation); bool success = RetryHelper.RetryOnException(10, TimeSpan.FromMilliseconds(200), false, () => { _logger?.LogTrace("Attempting to extracting save entries to the temporary location {TempPath}", tempLocation); ExtractFilesFromZip(tempFile, tempLocation); return true; }); if (!success) throw new ExtractionException("Could not extract the save archive. Is the file locked?"); manifest = await ManifestHelper.ReadAsync(tempLocation); #region Move files var tempLocationFilePath = "Files"; // Legacy support if (!Directory.Exists(Path.Combine(tempLocation, tempLocationFilePath))) tempLocationFilePath = "Saves"; foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == Enums.SavePathType.File)) { var entries = _client.Saves.GetFileSavePathEntries(savePath, installDirectory) ?? []; foreach (var entry in entries) { var entryPath = Path.Combine(tempLocation, tempLocationFilePath, savePath.Id.ToString(), entry.ArchivePath.Replace('/', Path.DirectorySeparatorChar)); var destinationPath = entry.ActualPath.ExpandEnvironmentVariables(installDirectory); if (File.Exists(entryPath)) { var destinationDirectory = Path.GetDirectoryName(destinationPath); Directory.CreateDirectory(destinationDirectory); // Handle individual files that were saved as an entry in the path if (File.Exists(destinationPath)) File.Delete(destinationPath); File.Move(entryPath, destinationPath); } else if (Directory.Exists(entryPath)) { // Handle directories that were saved as an entry in the path var entryFiles = Directory.GetFiles(entryPath, "*", SearchOption.AllDirectories); foreach (var entryFile in entryFiles) { var fileDestination = entryFile.Replace(entryPath, destinationPath); var destinationDirectory = Path.GetDirectoryName(fileDestination); Directory.CreateDirectory(destinationDirectory); if (File.Exists(fileDestination)) File.Delete(fileDestination); File.Move(entryFile, fileDestination); } } } } #endregion #region Handle registry importing var registryImportFilePaths = Directory.GetFiles(tempLocation, "_registry*.reg"); var importer = new RegistryImportUtility(); foreach (var registryImportFilePath in registryImportFilePaths) { var registryImportFileContents = File.ReadAllText(registryImportFilePath); var script = new PowerShellScript(Enums.ScriptType.SaveDownload); string adminArgument = string.Empty; if (registryImportFileContents.Contains("HKEY_LOCAL_MACHINE")) { script.AsAdmin(); adminArgument = " -Verb RunAs"; } script.UseInline($"Start-Process regedit.exe {adminArgument} -ArgumentList \"/s\", \"{registryImportFilePath}\""); if (_client.Scripts.Debug) { script.EnableDebug(); script.OnDebugStart = _client.Scripts.OnDebugStart; script.OnDebugBreak = _client.Scripts.OnDebugBreak; script.OnOutput = _client.Scripts.OnOutput; } await script.ExecuteAsync(); } #endregion // Clean up temp files Directory.Delete(tempLocation, true); } catch (Exception ex) { _logger?.LogError(ex, "The files in a save could not be extracted to their destination"); } finally { if (Directory.Exists(tempLocation)) Directory.Delete(tempLocation, true); } } } public async Task PackAsync(string installDirectory, GameManifest manifest) { using (var savePacker = new SavePacker(installDirectory)) { if (manifest?.SavePaths.Any() ?? false) savePacker.AddPaths(manifest.SavePaths); await savePacker.AddManifestAsync(manifest); return await savePacker.PackAsync(); } } public async Task UploadAsync(Stream stream, GameManifest manifest) { return await _client.UploadRequestAsync($"/api/Saves/Game/{manifest.Id}/Upload", stream); } public async Task UploadAsync(string installDirectory, Guid gameId) { using (var savePacker = new SavePacker(installDirectory)) { var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); if (manifest?.SavePaths?.Any() ?? false) savePacker.AddPaths(manifest.SavePaths); if (savePacker.HasEntries()) { await savePacker.AddManifestAsync(manifest); var stream = await savePacker.PackAsync(); await UploadAsync(stream, manifest); } } } public async Task DeleteAsync(Guid id) { await _client.DeleteRequestAsync($"/api/Saves/{id}"); } public IEnumerable GetFileSavePathEntries(SavePath savePath, string installDirectory) { IEnumerable localPaths; if (savePath.IsRegex) { var workingDirectory = GetLocalPath(savePath.WorkingDirectory, installDirectory); var pattern = savePath.Path; if (string.IsNullOrWhiteSpace(workingDirectory)) workingDirectory = installDirectory; if (Path.DirectorySeparatorChar == '\\') { pattern = pattern.Replace("\\", "\\\\"); pattern = pattern.Replace("/", "\\\\"); } var regex = new Regex(pattern); localPaths = Directory.GetFiles(workingDirectory, "*", SearchOption.AllDirectories) .Where(p => regex.IsMatch(p.Substring(workingDirectory.Length).TrimStart(Path.DirectorySeparatorChar))) .ToList(); } else { var workingDirectory = GetLocalPath(savePath.WorkingDirectory, installDirectory); var localPath = Path.Combine(workingDirectory, GetLocalPath(savePath.Path, installDirectory)); localPaths = new[] { localPath }; } var entries = new List(); foreach (var localPath in localPaths) { var actualPath = localPath.DeflateEnvironmentVariables(installDirectory); var workingDirectory = savePath.WorkingDirectory.DeflateEnvironmentVariables(installDirectory); var archivePath = actualPath.Replace(workingDirectory, "").TrimStart(Path.DirectorySeparatorChar); entries.Add(new SavePathEntry { ArchivePath = archivePath.Replace(Path.DirectorySeparatorChar, '/'), ActualPath = actualPath.Replace(Path.DirectorySeparatorChar, '/') }); savePath.Entries = entries; } return entries; } public string GetLocalPath(string path, string installDirectory) { var localPath = path.ExpandEnvironmentVariables(installDirectory); if (Path.DirectorySeparatorChar == '/') localPath = localPath.Replace('\\', Path.DirectorySeparatorChar); else localPath = localPath.Replace('/', Path.DirectorySeparatorChar); return localPath; } public string GetActualPath(string path, string installDirectory) { var actualPath = path.DeflateEnvironmentVariables(installDirectory); if (Path.DirectorySeparatorChar == '\\') actualPath = path.Replace('/', Path.DirectorySeparatorChar); return actualPath; } public string GetArchivePath(string path, string workingDirectory, string installDirectory) { path = GetLocalPath(path, installDirectory); workingDirectory = GetLocalPath(workingDirectory, installDirectory); var archivePath = path.Replace(workingDirectory, "").Trim(Path.DirectorySeparatorChar); if (Path.DirectorySeparatorChar == '\\') archivePath = archivePath.Replace(Path.DirectorySeparatorChar, '/'); return archivePath; } private void ExtractFilesFromZip(string zipPath, string destination) { using (var fs = File.OpenRead(zipPath)) using (var ts = new TrackableStream(fs)) using (var reader = ReaderFactory.Open(ts)) { reader.WriteAllToDirectory(destination, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }); } } } }