@@ -85,6 +85,7 @@ class ElectronLoader {
8585 "issues" : `${ ElectronLoader . APP_PACKAGE . repository } /issues` ,
8686 "paypal_donation" : "https://paypal.me/lVlyke"
8787 } ;
88+ static /** @type {string } */ APP_TMP_DIR = path . resolve ( path . join ( os . tmpdir ( ) , "SML" ) ) ;
8889 static /** @type {number } */ GAME_SCHEMA_VERSION = 1.1 ;
8990 static /** @type {string } */ GAME_DB_FILE = path . join ( __dirname , "game-db.json" ) ;
9091 static /** @type {string } */ GAME_RESOURCES_DIR = path . join ( __dirname , "resources" ) ;
@@ -382,19 +383,69 @@ class ElectronLoader {
382383
383384 ipcMain . handle ( "app:loadExternalProfile" , async (
384385 _event ,
385- /** @type {import("./app/models/app-message").AppMessageData<"app:loadExternalProfile"> } */ { profilePath }
386+ /** @type {import("./app/models/app-message").AppMessageData<"app:loadExternalProfile"> } */ { profilePath, directImport }
386387 ) => {
387388 if ( ! profilePath ) {
389+ const allowedExtensions = [ "json" ] ;
390+
391+ // Allow importing from archive if not doing a direct import
392+ if ( ! directImport ) {
393+ allowedExtensions . push ( "7z" , "7zip" , "zip" , "rar" )
394+ }
395+
388396 const pickedFile = ( await dialog . showOpenDialog ( {
389- properties : [ "openDirectory" ]
397+ properties : [ "openFile" ] ,
398+ filters : [ {
399+ name : "SML Profile" ,
400+ extensions : allowedExtensions
401+ } ]
390402 } ) ) ;
391403
392404 profilePath = pickedFile ?. filePaths [ 0 ] ;
393405 }
394406
407+ if ( ! profilePath ) {
408+ return null ;
409+ }
410+
411+ // If profile is uncompressed, use the dirname
412+ if ( profilePath . endsWith ( ".json" ) ) {
413+ profilePath = path . dirname ( profilePath ) ;
414+ } else {
415+ const archivePath = profilePath ;
416+ const profileName = path . basename ( archivePath , path . extname ( archivePath ) ) ;
417+
418+ try {
419+ const stagingDir = path . resolve ( path . join ( ElectronLoader . APP_TMP_DIR , profileName ) ) ;
420+ const _7zBinaryPath = this . #resolve7zBinaryPath( ) ;
421+
422+ // Clean the tmp staging dir
423+ await fs . remove ( stagingDir ) ;
424+
425+ // Decompress profile to staging dir
426+ await new Promise ( ( resolve , reject ) => {
427+ const decompressStream = Seven . extractFull ( archivePath , stagingDir , { $bin : _7zBinaryPath } ) ;
428+ decompressStream . on ( "end" , ( ) => resolve ( true ) ) ;
429+ decompressStream . on ( "error" , ( e ) => reject ( e ) ) ;
430+ } ) ;
431+
432+ profilePath = stagingDir ;
433+ } catch ( e ) {
434+ log . error ( "Failed to load external profile from path:" , profilePath , e ) ;
435+ return null ;
436+ }
437+ }
438+
439+ // Attempt to load the profile
395440 if ( profilePath ) {
396441 profilePath = path . resolve ( profilePath ) ; // Make sure path is absolute
397- return this . loadProfileFromPath ( profilePath , profilePath ) ;
442+ const loadedProfile = this . loadProfileFromPath ( profilePath , profilePath ) ;
443+
444+ if ( ! loadedProfile ) {
445+ log . error ( "Failed to load external profile from path:" , profilePath ) ;
446+ }
447+
448+ return loadedProfile ;
398449 }
399450 } ) ;
400451
@@ -412,34 +463,52 @@ class ElectronLoader {
412463 const profileDir = this . getProfileDir ( profile ) ;
413464 const defaultProfileDir = this . getDefaultProfileDir ( profile . name ) ;
414465
415- if ( profileDir === defaultProfileDir ) {
416- /** @type { string | undefined } */ let exportFolder = undefined ;
466+ // Choose path to save profile archive
467+ const exportFilePath = ( await dialog . showSaveDialog ( {
468+ defaultPath : profile . name ,
469+ filters : [ {
470+ name : "Exported Profile" ,
471+ extensions : [ "7z" ]
472+ } ]
473+ } ) ) ?. filePath ;
417474
418- // Pick a path that isn't in the profiles directory
419- do {
420- const exportFolderPick = ( await dialog . showOpenDialog ( {
421- properties : [ "openDirectory" ]
422- } ) ) ;
423-
424- exportFolder = exportFolderPick ?. filePaths [ 0 ] ;
425- } while ( exportFolder && path . resolve ( exportFolder ) . startsWith ( path . resolve ( ElectronLoader . APP_PROFILES_DIR ) ) ) ;
475+ if ( ! exportFilePath ) {
476+ return undefined ;
477+ }
426478
427- if ( ! exportFolder ) {
428- return undefined ;
429- }
479+ const initialCwd = process . cwd ( ) ;
480+ // Compress the profile data to archive
481+ try {
482+ const _7zBinaryPath = this . #resolve7zBinaryPath( ) ;
430483
431- // Move profile to the new folder
432- fs . moveSync ( profileDir , exportFolder , { overwrite : true } ) ;
484+ process . chdir ( path . resolve ( profileDir ) ) ;
433485
434- return exportFolder ;
435- } else if ( fs . existsSync ( defaultProfileDir ) ) {
436- // If the profile is located at a non-default path, we just need to remove its symlink
437- fs . removeSync ( defaultProfileDir ) ;
486+ await new Promise ( ( resolve , reject ) => {
487+ const compressStream = Seven . add ( exportFilePath , "." , {
488+ $bin : _7zBinaryPath ,
489+ recursive : true
490+ } ) ;
491+
492+ compressStream . on ( "end" , ( ) => resolve ( true ) ) ;
493+ compressStream . on ( "error" , ( e ) => reject ( e ) ) ;
494+ } ) ;
495+ } catch ( e ) {
496+ log . error ( "Failed to export profile: " , e ) ;
497+ return undefined ;
498+ } finally {
499+ process . chdir ( initialCwd ) ;
500+ }
438501
439- return profileDir ;
502+ // Remove profile from SML and back up files to app tmp dir
503+ const backupDir = path . join ( ElectronLoader . APP_TMP_DIR , `${ profile . name } .bak_${ this . #asFileName( new Date ( ) . toISOString ( ) ) } ` ) ;
504+ await fs . move ( profileDir , backupDir , { overwrite : true } ) ;
505+
506+ // Remove any symlinks to profile
507+ if ( profileDir !== defaultProfileDir ) {
508+ await fs . remove ( defaultProfileDir ) ;
440509 }
441510
442- return undefined ;
511+ return exportFilePath ;
443512 } ) ;
444513
445514 ipcMain . handle ( "app:deleteProfile" , async (
@@ -2322,45 +2391,7 @@ class ElectronLoader {
23222391 case ".zip" :
23232392 case ".rar" : {
23242393 decompressOperation = new Promise ( ( resolve , _reject ) => {
2325- // Look for 7-Zip installed on system
2326- const _7zBinaries = [
2327- "7zzs" ,
2328- "7zz" ,
2329- "7z" ,
2330- "7z.exe"
2331- ] ;
2332-
2333- const _7zBinaryLocations = [
2334- "C:\\Program Files\\7-Zip\\7z.exe" ,
2335- "C:\\Program Files (x86)\\7-Zip\\7z.exe"
2336- ] ;
2337-
2338- let _7zBinaryPath = _7zBinaryLocations . find ( _7zPath => fs . existsSync ( _7zPath ) ) ;
2339-
2340- if ( ! _7zBinaryPath ) {
2341- _7zBinaryPath = _7zBinaries . reduce ( ( _7zBinaryPath , _7zBinaryPathGuess ) => {
2342- try {
2343- const which7zBinaryPath = which . sync ( _7zBinaryPathGuess ) ;
2344- _7zBinaryPath = ( Array . isArray ( which7zBinaryPath )
2345- ? which7zBinaryPath [ 0 ]
2346- : which7zBinaryPath
2347- ) ?? undefined ;
2348- } catch ( _err ) { }
2349-
2350- return _7zBinaryPath ;
2351- } , _7zBinaryPath ) ;
2352- }
2353-
2354- if ( ! _7zBinaryPath ) {
2355- // Fall back to bundled 7-Zip binary if it's not found on system
2356- // TODO - Warn user about opening RARs if 7-Zip not installed on machine
2357- _7zBinaryPath = sevenBin . path7za ;
2358-
2359- log . warn ( "7-Zip binary was not found on this machine. Falling back to bundled binary." ) ;
2360- } else {
2361- log . info ( "Found 7-Zip binary: " , _7zBinaryPath ) ;
2362- }
2363-
2394+ const _7zBinaryPath = this . #resolve7zBinaryPath( ) ;
23642395 const decompressStream = Seven . extractFull ( filePath , modDirStagingPath , { $bin : _7zBinaryPath } ) ;
23652396 decompressStream . on ( "end" , ( ) => resolve ( true ) ) ;
23662397 decompressStream . on ( "error" , ( e ) => {
@@ -4413,6 +4444,51 @@ class ElectronLoader {
44134444 return "" ;
44144445 }
44154446
4447+ /** @return {string } */
4448+ #resolve7zBinaryPath( ) {
4449+ // Look for 7-Zip installed on system
4450+ const _7zBinaries = [
4451+ "7zzs" ,
4452+ "7zz" ,
4453+ "7z" ,
4454+ "7z.exe"
4455+ ] ;
4456+
4457+ const _7zBinaryLocations = [
4458+ "C:\\Program Files\\7-Zip\\7z.exe" ,
4459+ "C:\\Program Files (x86)\\7-Zip\\7z.exe"
4460+ ] ;
4461+
4462+ let _7zBinaryPath = _7zBinaryLocations . find ( _7zPath => fs . existsSync ( _7zPath ) ) ;
4463+
4464+ if ( ! _7zBinaryPath ) {
4465+ _7zBinaryPath = _7zBinaries . reduce ( ( _7zBinaryPath , _7zBinaryPathGuess ) => {
4466+ try {
4467+ const which7zBinaryPath = which . sync ( _7zBinaryPathGuess ) ;
4468+ _7zBinaryPath = ( Array . isArray ( which7zBinaryPath )
4469+ ? which7zBinaryPath [ 0 ]
4470+ : which7zBinaryPath
4471+ ) ?? undefined ;
4472+ } catch ( _err ) { }
4473+
4474+ return _7zBinaryPath ;
4475+ } , _7zBinaryPath ) ;
4476+ }
4477+
4478+ if ( ! _7zBinaryPath ) {
4479+ // Fall back to bundled 7-Zip binary if it's not found on system
4480+ // TODO - Warn user about opening RARs if 7-Zip not installed on machine
4481+ _7zBinaryPath = sevenBin . path7za ;
4482+
4483+ log . warn ( "7-Zip binary was not found on this machine. Falling back to bundled binary." ) ;
4484+ log . warn ( "NOTE: RAR archives can not be read using the bundled binary. Install 7-Zip to read RAR archives." ) ;
4485+ } else {
4486+ log . info ( "Found 7-Zip binary: " , _7zBinaryPath ) ;
4487+ }
4488+
4489+ return _7zBinaryPath ;
4490+ }
4491+
44164492 /** @return {string } */
44174493 #formatLogData( logData ) {
44184494 return logData ?. map ( arg => this . #formatLogArg( arg ) ) . join ( " " ) ?? "" ;
0 commit comments