2024-10-04 23:37:49 -05:00
using LANCommander.SDK.Extensions ;
2023-11-10 01:32:30 -06:00
using LANCommander.SDK.Helpers ;
using LANCommander.SDK.Models ;
2023-11-16 00:47:01 -06:00
using LANCommander.SDK.PowerShell ;
2023-12-26 18:05:27 -06:00
using Microsoft.Extensions.Logging ;
2023-04-07 19:09:00 -05:00
using SharpCompress.Common ;
using SharpCompress.Readers ;
2023-03-29 20:49:31 -05:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2023-11-30 18:25:37 -06:00
using System.Text.RegularExpressions ;
2023-12-26 18:07:06 -06:00
using System.Threading.Tasks ;
2025-10-06 20:29:34 -05:00
using LANCommander.SDK.Abstractions ;
2025-09-22 00:29:51 -05:00
using LANCommander.SDK.Factories ;
2025-03-18 20:30:04 -05:00
using LANCommander.SDK.Utilities ;
2025-09-22 00:29:51 -05:00
using Action = System . Action ;
2023-03-29 20:49:31 -05:00
2025-08-18 03:03:33 -05:00
// Some terms for this file since they're probably going to be needed in the future:
2023-12-23 17:39:37 -06:00
// 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
2024-10-04 23:37:49 -05:00
namespace LANCommander.SDK.Services
2023-03-29 20:49:31 -05:00
{
2025-09-24 20:58:23 -05:00
public class SaveClient (
2025-09-22 00:29:51 -05:00
ApiRequestFactory apiRequestFactory ,
2025-10-06 20:29:34 -05:00
ISettingsProvider settingsProvider ,
2025-11-26 01:09:52 -06:00
PowerShellScriptFactory powerShellScriptFactory ,
2025-09-24 20:58:23 -05:00
ILogger < SaveClient > logger )
2023-03-29 20:49:31 -05:00
{
2023-11-10 01:32:30 -06:00
public delegate void OnDownloadProgressHandler ( DownloadProgressChangedEventArgs e ) ;
public event OnDownloadProgressHandler OnDownloadProgress ;
2025-09-22 00:29:51 -05:00
public delegate void OnDownloadCompleteHandler ( ) ;
2023-11-10 01:32:30 -06:00
public event OnDownloadCompleteHandler OnDownloadComplete ;
2025-09-22 00:29:51 -05:00
private async Task < FileInfo > DownloadAsync ( Guid id , Action < DownloadProgressChangedEventArgs > progressHandler , Action completeHandler )
2023-12-26 18:05:27 -06:00
{
2025-09-22 00:29:51 -05:00
var destination = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
return await apiRequestFactory
. Create ( )
. UseAuthenticationToken ( )
. UseVersioning ( )
. UseRoute ( $"/api/Saves/{id}/Download" )
. OnProgress ( progressHandler )
. OnComplete ( completeHandler )
. DownloadAsync ( destination ) ;
2023-12-26 18:05:27 -06:00
}
2025-09-22 00:29:51 -05:00
public async Task < FileInfo > DownloadLatestAsync ( Guid gameId , Action < DownloadProgressChangedEventArgs > progressHandler , Action completeHandler )
2024-01-02 02:34:58 -06:00
{
2025-09-22 00:29:51 -05:00
var destination = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
return await apiRequestFactory
. Create ( )
. UseAuthenticationToken ( )
. UseVersioning ( )
. UseRoute ( $"/api/Saves/Game/{gameId}/Latest/Download" )
. OnProgress ( progressHandler )
. OnComplete ( completeHandler )
. DownloadAsync ( destination ) ;
2024-02-17 17:30:14 -06:00
}
2024-10-23 18:17:01 -05:00
public async Task < IEnumerable < GameSave > > GetAsync ( Guid gameId )
{
2025-09-22 00:29:51 -05:00
return await apiRequestFactory
. Create ( )
. UseAuthenticationToken ( )
. UseVersioning ( )
. UseRoute ( $"/api/Saves/Game/{gameId}" )
. GetAsync < IEnumerable < GameSave > > ( ) ;
2024-02-06 19:36:55 -06:00
}
2025-09-22 00:29:51 -05:00
public async Task < GameSave > GetLatestAsync ( Guid gameId )
2024-07-15 18:17:04 -05:00
{
2025-09-22 00:29:51 -05:00
return await apiRequestFactory
. Create ( )
. UseAuthenticationToken ( )
. UseVersioning ( )
. UseRoute ( $"/api/Saves/Game/{gameId}/Latest" )
. GetAsync < GameSave > ( ) ;
2024-07-15 18:17:04 -05:00
}
2024-08-07 01:14:13 -05:00
public async Task DownloadAsync ( string installDirectory , Guid gameId , Guid ? saveId = null )
2023-03-29 20:49:31 -05:00
{
2025-11-29 18:24:27 -06:00
var manifest = await ManifestHelper . ReadAsync < SDK . Models . Manifest . Game > ( installDirectory , gameId ) ;
2023-11-10 01:32:30 -06:00
2025-08-18 03:03:28 -05:00
string tempFile ;
2024-10-04 23:37:49 -05:00
string tempLocation = string . Empty ;
2023-03-29 20:49:31 -05:00
2023-11-10 01:32:30 -06:00
if ( manifest ! = null )
2023-03-29 20:49:31 -05:00
{
2025-09-22 00:29:51 -05:00
FileInfo destination ;
2024-02-17 17:30:14 -06:00
if ( ! saveId . HasValue )
2023-03-29 20:49:31 -05:00
{
2024-08-07 01:14:13 -05:00
destination = await DownloadLatestAsync ( manifest . Id , ( changed ) = >
2024-02-17 17:30:14 -06:00
{
OnDownloadProgress ? . Invoke ( changed ) ;
2025-09-22 00:29:51 -05:00
} , ( ) = >
2024-02-17 17:30:14 -06:00
{
2025-09-22 00:29:51 -05:00
OnDownloadComplete ? . Invoke ( ) ;
2024-02-17 17:30:14 -06:00
} ) ;
}
else
2023-03-29 20:49:31 -05:00
{
2024-08-07 01:14:13 -05:00
destination = await DownloadAsync ( saveId . Value , ( changed ) = >
2024-02-17 17:30:14 -06:00
{
OnDownloadProgress ? . Invoke ( changed ) ;
2025-09-22 00:29:51 -05:00
} , ( ) = >
2024-02-17 17:30:14 -06:00
{
2025-09-22 00:29:51 -05:00
OnDownloadComplete ? . Invoke ( ) ;
2024-02-17 17:30:14 -06:00
} ) ;
}
2025-09-22 00:29:51 -05:00
if ( ! destination . Exists )
2026-05-25 04:16:19 -05:00
{
logger ? . LogWarning ( "Save archive was not downloaded for game {GameId}" , gameId ) ;
2023-12-27 17:22:36 -06:00
return ;
2026-05-25 04:16:19 -05:00
}
2023-12-27 17:22:36 -06:00
2026-05-25 04:16:19 -05:00
logger ? . LogDebug ( "Save archive downloaded to {SaveTempLocation} for game {GameId}" , destination , gameId ) ;
2023-12-26 18:07:06 -06:00
2025-09-22 00:29:51 -05:00
tempFile = destination . FullName ;
2023-11-10 01:32:30 -06:00
2023-03-29 20:49:31 -05:00
// Go into the archive and extract the files to the correct locations
try
{
2023-12-26 18:07:06 -06:00
tempLocation = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
2023-03-29 20:49:31 -05:00
Directory . CreateDirectory ( tempLocation ) ;
2024-10-04 23:37:49 -05:00
bool success = RetryHelper . RetryOnException ( 10 , TimeSpan . FromMilliseconds ( 200 ) , false , ( ) = >
2023-12-26 18:07:06 -06:00
{
2026-05-25 04:16:19 -05:00
logger ? . LogTrace ( "Extracting save archive to temporary location {TempPath}" , tempLocation ) ;
2023-12-26 18:07:06 -06:00
ExtractFilesFromZip ( tempFile , tempLocation ) ;
return true ;
} ) ;
if ( ! success )
throw new ExtractionException ( "Could not extract the save archive. Is the file locked?" ) ;
2025-11-29 18:24:27 -06:00
manifest = await ManifestHelper . ReadAsync < SDK . Models . Manifest . Game > ( tempLocation ) ;
2023-03-29 20:49:31 -05:00
#region Move files
2024-03-28 19:59:15 -05:00
var tempLocationFilePath = "Files" ;
// Legacy support
if ( ! Directory . Exists ( Path . Combine ( tempLocation , tempLocationFilePath ) ) )
tempLocationFilePath = "Saves" ;
2026-07-08 02:15:54 -05:00
foreach ( var savePath in manifest . SavePaths . Where ( sp = > sp . Type = = Enums . SavePathType . File & & EnvironmentHelper . SupportsCurrentRuntime ( sp . Platforms ) ) )
2023-03-29 20:49:31 -05:00
{
2025-09-22 00:29:51 -05:00
var entries = GetFileSavePathEntries ( savePath , installDirectory ) ? ? [ ] ;
2025-05-24 18:50:51 +02:00
2026-05-25 04:16:19 -05:00
logger ? . LogTrace ( "Processing save path {SavePathId} with {EntryCount} entries" , savePath . Id , entries . Count ( ) ) ;
2025-05-24 18:50:51 +02:00
foreach ( var entry in entries )
2023-03-29 20:49:31 -05:00
{
2024-03-28 19:59:15 -05:00
var entryPath = Path . Combine ( tempLocation , tempLocationFilePath , savePath . Id . ToString ( ) , entry . ArchivePath . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
2023-12-26 18:07:06 -06:00
var destinationPath = entry . ActualPath . ExpandEnvironmentVariables ( installDirectory ) ;
2023-03-29 20:49:31 -05:00
2023-12-26 18:07:06 -06:00
if ( File . Exists ( entryPath ) )
2023-04-02 20:50:57 -05:00
{
2025-05-24 18:50:51 +02:00
var destinationDirectory = Path . GetDirectoryName ( destinationPath ) ;
2024-10-15 02:00:54 -05:00
Directory . CreateDirectory ( destinationDirectory ) ;
2023-12-26 18:07:06 -06:00
// Handle individual files that were saved as an entry in the path
if ( File . Exists ( destinationPath ) )
File . Delete ( destinationPath ) ;
2023-11-30 18:30:15 -06:00
2023-12-26 18:07:06 -06:00
File . Move ( entryPath , destinationPath ) ;
2026-05-25 04:16:19 -05:00
logger ? . LogTrace ( "Restored save file {ArchivePath} to {DestinationPath}" , entry . ArchivePath , destinationPath ) ;
2023-11-30 18:37:23 -06:00
}
2023-12-26 18:07:06 -06:00
else if ( Directory . Exists ( entryPath ) )
2023-11-30 18:37:23 -06:00
{
2023-12-26 18:07:06 -06:00
// Handle directories that were saved as an entry in the path
var entryFiles = Directory . GetFiles ( entryPath , "*" , SearchOption . AllDirectories ) ;
2023-11-30 18:30:15 -06:00
2023-12-26 18:07:06 -06:00
foreach ( var entryFile in entryFiles )
2023-11-30 18:30:15 -06:00
{
2025-05-24 18:50:51 +02:00
var fileDestination = entryFile . Replace ( entryPath , destinationPath ) ;
2026-05-25 04:16:19 -05:00
2025-05-24 18:50:51 +02:00
var destinationDirectory = Path . GetDirectoryName ( fileDestination ) ;
2024-10-15 02:00:54 -05:00
Directory . CreateDirectory ( destinationDirectory ) ;
2023-12-26 18:07:06 -06:00
if ( File . Exists ( fileDestination ) )
File . Delete ( fileDestination ) ;
File . Move ( entryFile , fileDestination ) ;
2023-11-30 18:30:15 -06:00
}
2026-05-25 04:16:19 -05:00
logger ? . LogTrace ( "Restored save directory {ArchivePath} ({FileCount} files) to {DestinationPath}" , entry . ArchivePath , entryFiles . Length , destinationPath ) ;
}
else
{
logger ? . LogWarning ( "Save entry {ArchivePath} not found in archive at {EntryPath}" , entry . ArchivePath , entryPath ) ;
2023-04-02 20:50:57 -05:00
}
2023-03-29 20:49:31 -05:00
}
}
#endregion
#region Handle registry importing
2025-06-28 00:10:54 +02:00
var registryImportFilePaths = Directory . GetFiles ( tempLocation , "_registry*.reg" ) ;
var importer = new RegistryImportUtility ( ) ;
2023-03-29 20:49:31 -05:00
2025-06-28 00:10:54 +02:00
foreach ( var registryImportFilePath in registryImportFilePaths )
2023-03-29 20:49:31 -05:00
{
var registryImportFileContents = File . ReadAllText ( registryImportFilePath ) ;
2025-11-26 01:09:52 -06:00
var script = powerShellScriptFactory . Create ( Enums . ScriptType . SaveDownload ) ;
2023-11-16 00:47:01 -06:00
2025-06-27 23:57:03 +02:00
string adminArgument = string . Empty ;
2023-11-16 00:47:01 -06:00
if ( registryImportFileContents . Contains ( "HKEY_LOCAL_MACHINE" ) )
2025-06-27 23:57:03 +02:00
{
2024-08-08 18:00:41 -05:00
script . AsAdmin ( ) ;
2025-06-27 23:57:03 +02:00
adminArgument = " -Verb RunAs" ;
}
script . UseInline ( $"Start-Process regedit.exe {adminArgument} -ArgumentList \" / s \ ", \"{registryImportFilePath}\"" ) ;
2023-11-16 00:47:01 -06:00
2025-10-06 20:29:34 -05:00
if ( settingsProvider . CurrentValue . Debug . EnableScriptDebugging )
2025-06-27 23:20:10 +02:00
{
2024-08-16 18:36:17 -05:00
script . EnableDebug ( ) ;
2025-06-27 23:20:10 +02:00
}
2024-08-16 18:36:17 -05:00
2024-09-20 00:27:20 -05:00
await script . ExecuteAsync < int > ( ) ;
2023-03-29 20:49:31 -05:00
}
#endregion
// Clean up temp files
Directory . Delete ( tempLocation , true ) ;
}
catch ( Exception ex )
{
2026-05-25 04:16:19 -05:00
logger ? . LogError ( ex , "Failed to extract save files for game {GameId}" , gameId ) ;
2023-12-26 18:07:06 -06:00
}
finally
{
if ( Directory . Exists ( tempLocation ) )
Directory . Delete ( tempLocation , true ) ;
2023-03-29 20:49:31 -05:00
}
}
}
2025-11-29 18:24:27 -06:00
public async Task < Stream > PackAsync ( string installDirectory , SDK . Models . Manifest . Game manifest )
2023-03-29 20:49:31 -05:00
{
2025-03-18 20:30:04 -05:00
using ( var savePacker = new SavePacker ( installDirectory ) )
2023-11-10 01:32:30 -06:00
{
2025-03-18 20:30:04 -05:00
if ( manifest ? . SavePaths . Any ( ) ? ? false )
2026-07-08 02:15:54 -05:00
savePacker . AddPaths ( manifest . SavePaths . Where ( sp = > EnvironmentHelper . SupportsCurrentRuntime ( sp . Platforms ) ) ) ;
2023-03-29 20:49:31 -05:00
2025-03-18 20:30:04 -05:00
await savePacker . AddManifestAsync ( manifest ) ;
2026-07-08 02:15:54 -05:00
2025-03-18 20:30:04 -05:00
return await savePacker . PackAsync ( ) ;
}
}
2023-03-29 20:49:31 -05:00
2025-11-29 18:24:27 -06:00
public async Task < GameSave > UploadAsync ( Stream stream , SDK . Models . Manifest . Game manifest )
2025-03-19 17:35:16 -05:00
{
2025-09-22 00:29:51 -05:00
return await apiRequestFactory
. Create ( )
. UseAuthenticationToken ( )
. UseVersioning ( )
2026-07-08 02:15:54 -05:00
. UseRoute ( $"/api/Saves/Game/{manifest.Id}/Upload?platform={EnvironmentHelper.GetCurrentRuntime()}" )
2025-12-04 13:35:05 +11:00
. UploadAsync < GameSave > ( $"game-{manifest.Id}-save" , stream ) ;
2025-03-19 17:35:16 -05:00
}
2025-08-18 03:03:28 -05:00
public async Task UploadAsync ( string installDirectory , Guid gameId )
2025-03-18 20:30:04 -05:00
{
using ( var savePacker = new SavePacker ( installDirectory ) )
{
2025-11-29 18:24:27 -06:00
var manifest = await ManifestHelper . ReadAsync < SDK . Models . Manifest . Game > ( installDirectory , gameId ) ;
2023-11-30 18:25:37 -06:00
2025-06-25 23:25:26 +02:00
if ( manifest ? . SavePaths ? . Any ( ) ? ? false )
2026-07-08 02:15:54 -05:00
savePacker . AddPaths ( manifest . SavePaths . Where ( sp = > EnvironmentHelper . SupportsCurrentRuntime ( sp . Platforms ) ) ) ;
2023-03-29 20:49:31 -05:00
2025-06-25 23:25:26 +02:00
if ( savePacker . HasEntries ( ) )
{
2026-05-25 04:16:19 -05:00
logger ? . LogDebug ( "Packing {EntryCount} save entries for game {GameId}" , savePacker . EntryCount , gameId ) ;
2025-06-25 23:25:26 +02:00
await savePacker . AddManifestAsync ( manifest ) ;
var stream = await savePacker . PackAsync ( ) ;
2025-08-18 03:03:28 -05:00
await UploadAsync ( stream , manifest ) ;
2026-05-25 04:16:19 -05:00
logger ? . LogDebug ( "Save uploaded for game {GameId} ({Size} bytes)" , gameId , stream . Length ) ;
}
else
{
logger ? . LogDebug ( "No save files found to upload for game {GameId}" , gameId ) ;
2025-06-25 23:25:26 +02:00
}
2023-03-29 20:49:31 -05:00
}
}
2024-10-23 18:17:01 -05:00
public async Task DeleteAsync ( Guid id )
2024-02-17 17:30:14 -06:00
{
2025-09-22 00:29:51 -05:00
await apiRequestFactory
. Create ( )
. UseAuthenticationToken ( )
. UseVersioning ( )
. UseRoute ( $"/api/Saves/{id}" )
. DeleteAsync < bool > ( ) ;
2024-02-17 17:30:14 -06:00
}
2025-11-29 18:24:27 -06:00
public IEnumerable < SavePathEntry > GetFileSavePathEntries ( SDK . Models . Manifest . SavePath savePath , string installDirectory )
2023-12-23 02:00:53 -06:00
{
IEnumerable < string > localPaths ;
if ( savePath . IsRegex )
{
var workingDirectory = GetLocalPath ( savePath . WorkingDirectory , installDirectory ) ;
var pattern = savePath . Path ;
2024-10-04 23:37:49 -05:00
if ( string . IsNullOrWhiteSpace ( workingDirectory ) )
2023-12-23 02:00:53 -06:00
workingDirectory = installDirectory ;
var regex = new Regex ( pattern ) ;
localPaths = Directory . GetFiles ( workingDirectory , "*" , SearchOption . AllDirectories )
2026-05-28 22:46:24 -05:00
. Where ( p = >
{
var relativePath = p . Substring ( workingDirectory . Length )
. TrimStart ( Path . DirectorySeparatorChar )
. Replace ( '\\' , '/' ) ;
return regex . IsMatch ( relativePath ) ;
} )
2023-12-23 02:00:53 -06:00
. ToList ( ) ;
}
else
{
var workingDirectory = GetLocalPath ( savePath . WorkingDirectory , installDirectory ) ;
var localPath = Path . Combine ( workingDirectory , GetLocalPath ( savePath . Path , installDirectory ) ) ;
2025-08-18 03:03:28 -05:00
localPaths = new [ ] { localPath } ;
2023-12-23 02:00:53 -06:00
}
var entries = new List < SavePathEntry > ( ) ;
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 ;
}
2023-12-23 00:11:16 -06:00
public string GetLocalPath ( string path , string installDirectory )
{
2023-12-23 17:39:37 -06:00
var localPath = path . ExpandEnvironmentVariables ( installDirectory ) ;
2023-12-23 00:11:16 -06:00
if ( Path . DirectorySeparatorChar = = '/' )
localPath = localPath . Replace ( '\\' , Path . DirectorySeparatorChar ) ;
else
localPath = localPath . Replace ( '/' , Path . DirectorySeparatorChar ) ;
return localPath ;
}
2023-12-23 17:39:37 -06:00
public string GetActualPath ( string path , string installDirectory )
2023-12-23 00:11:16 -06:00
{
2023-12-23 17:39:37 -06:00
var actualPath = path . DeflateEnvironmentVariables ( installDirectory ) ;
2023-12-23 00:11:16 -06:00
if ( Path . DirectorySeparatorChar = = '\\' )
2025-08-18 03:03:06 -05:00
actualPath = path . Replace ( '/' , Path . DirectorySeparatorChar ) ;
2023-12-23 00:11:16 -06:00
2023-12-23 17:39:37 -06:00
return actualPath ;
2023-12-23 00:11:16 -06:00
}
2023-12-23 17:39:37 -06:00
public string GetArchivePath ( string path , string workingDirectory , string installDirectory )
2023-03-29 20:49:31 -05:00
{
2023-12-23 17:39:37 -06:00
path = GetLocalPath ( path , installDirectory ) ;
workingDirectory = GetLocalPath ( workingDirectory , installDirectory ) ;
2023-03-29 20:49:31 -05:00
2023-12-23 17:39:37 -06:00
var archivePath = path . Replace ( workingDirectory , "" ) . Trim ( Path . DirectorySeparatorChar ) ;
2023-03-29 20:49:31 -05:00
2023-12-23 17:39:37 -06:00
if ( Path . DirectorySeparatorChar = = '\\' )
archivePath = archivePath . Replace ( Path . DirectorySeparatorChar , '/' ) ;
return archivePath ;
}
2023-03-29 20:49:31 -05:00
private void ExtractFilesFromZip ( string zipPath , string destination )
{
2023-04-07 19:09:00 -05:00
using ( var fs = File . OpenRead ( zipPath ) )
2025-10-09 02:16:20 -05:00
using ( var ts = new TrackableStream ( fs , fs . Length ) )
2026-03-09 22:12:01 -05:00
using ( var reader = ReaderFactory . OpenReader ( ts , new ReaderOptions ( ) ) )
2023-03-29 20:49:31 -05:00
{
2023-04-07 19:09:00 -05:00
reader . WriteAllToDirectory ( destination , new ExtractionOptions ( )
2023-03-29 20:49:31 -05:00
{
2023-04-07 19:09:00 -05:00
ExtractFullPath = true ,
Overwrite = true
} ) ;
2023-03-29 20:49:31 -05:00
}
}
}
}