2022-10-13 03:41:21 -04:00
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System ;
using System.Buffers ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using System.IO.Compression ;
using System.Linq ;
using System.Text ;
using System.Text.Json ;
using System.Text.Json.Nodes ;
using System.Text.RegularExpressions ;
using System.Threading ;
2024-10-17 05:14:57 -04:00
2023-03-21 10:27:29 +01:00
using ManagedCommon ;
2022-10-13 03:41:21 -04:00
using Microsoft.PowerToys.Settings.UI.Library.Utilities ;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class SettingsBackupAndRestoreUtils
{
private static SettingsBackupAndRestoreUtils instance ;
2022-12-18 14:27:14 +01:00
private ( bool Success , string Severity , bool LastBackupExists , DateTime ? LastRan ) lastBackupSettingsResults ;
2024-11-13 12:36:45 -05:00
private static Lock backupSettingsInternalLock = new Lock ( ) ;
private static Lock removeOldBackupsLock = new Lock ( ) ;
2022-10-13 03:41:21 -04:00
public DateTime LastBackupStartTime { get ; set ; }
2023-12-28 13:37:13 +03:00
private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{
WriteIndented = true ,
} ;
2022-10-13 03:41:21 -04:00
private SettingsBackupAndRestoreUtils ( )
{
LastBackupStartTime = DateTime . MinValue ;
}
public static SettingsBackupAndRestoreUtils Instance
{
get
{
if ( instance = = null )
{
instance = new SettingsBackupAndRestoreUtils ( ) ;
}
return instance ;
}
}
2023-03-16 15:51:31 +01:00
private sealed class JsonMergeHelper
2022-10-13 03:41:21 -04:00
{
// mostly from https://stackoverflow.com/questions/58694837/system-text-json-merge-two-objects
// but with some update to prevent array item duplicates
public static string Merge ( string originalJson , string newContent )
{
var outputBuffer = new ArrayBufferWriter < byte > ( ) ;
using ( JsonDocument jDoc1 = JsonDocument . Parse ( originalJson ) )
using ( JsonDocument jDoc2 = JsonDocument . Parse ( newContent ) )
using ( var jsonWriter = new Utf8JsonWriter ( outputBuffer , new JsonWriterOptions { Indented = true } ) )
{
JsonElement root1 = jDoc1 . RootElement ;
JsonElement root2 = jDoc2 . RootElement ;
if ( root1 . ValueKind ! = JsonValueKind . Array & & root1 . ValueKind ! = JsonValueKind . Object )
{
throw new InvalidOperationException ( $"The original JSON document to merge new content into must be a container type. Instead it is {root1.ValueKind}." ) ;
}
if ( root1 . ValueKind ! = root2 . ValueKind )
{
return originalJson ;
}
if ( root1 . ValueKind = = JsonValueKind . Array )
{
MergeArrays ( jsonWriter , root1 , root2 , false ) ;
}
else
{
MergeObjects ( jsonWriter , root1 , root2 ) ;
}
}
return Encoding . UTF8 . GetString ( outputBuffer . WrittenSpan ) ;
}
private static void MergeObjects ( Utf8JsonWriter jsonWriter , JsonElement root1 , JsonElement root2 )
{
jsonWriter . WriteStartObject ( ) ;
// Write all the properties of the first document.
// If a property exists in both documents, either:
// * Merge them, if the value kinds match (e.g. both are objects or arrays),
// * Completely override the value of the first with the one from the second, if the value kind mismatches (e.g. one is object, while the other is an array or string),
// * Or favor the value of the first (regardless of what it may be), if the second one is null (i.e. don't override the first).
foreach ( JsonProperty property in root1 . EnumerateObject ( ) )
{
string propertyName = property . Name ;
JsonValueKind newValueKind ;
if ( root2 . TryGetProperty ( propertyName , out JsonElement newValue ) & & ( newValueKind = newValue . ValueKind ) ! = JsonValueKind . Null )
{
jsonWriter . WritePropertyName ( propertyName ) ;
JsonElement originalValue = property . Value ;
JsonValueKind originalValueKind = originalValue . ValueKind ;
if ( newValueKind = = JsonValueKind . Object & & originalValueKind = = JsonValueKind . Object )
{
MergeObjects ( jsonWriter , originalValue , newValue ) ; // Recursive call
}
else if ( newValueKind = = JsonValueKind . Array & & originalValueKind = = JsonValueKind . Array )
{
MergeArrays ( jsonWriter , originalValue , newValue , false ) ;
}
else
{
newValue . WriteTo ( jsonWriter ) ;
}
}
else
{
property . WriteTo ( jsonWriter ) ;
}
}
// Write all the properties of the second document that are unique to it.
foreach ( JsonProperty property in root2 . EnumerateObject ( ) )
{
if ( ! root1 . TryGetProperty ( property . Name , out _ ) )
{
property . WriteTo ( jsonWriter ) ;
}
}
jsonWriter . WriteEndObject ( ) ;
}
private static void MergeArrays ( Utf8JsonWriter jsonWriter , JsonElement root1 , JsonElement root2 , bool allowDupes )
{
// just does one level!!!
jsonWriter . WriteStartArray ( ) ;
if ( allowDupes )
{
// Write all the elements from both JSON arrays
foreach ( JsonElement element in root1 . EnumerateArray ( ) )
{
element . WriteTo ( jsonWriter ) ;
}
foreach ( JsonElement element in root2 . EnumerateArray ( ) )
{
element . WriteTo ( jsonWriter ) ;
}
}
else
{
var arrayItems = new HashSet < string > ( ) ;
foreach ( JsonElement element in root1 . EnumerateArray ( ) )
{
element . WriteTo ( jsonWriter ) ;
arrayItems . Add ( element . ToString ( ) ) ;
}
foreach ( JsonElement element in root2 . EnumerateArray ( ) )
{
if ( ! arrayItems . Contains ( element . ToString ( ) ) )
{
element . WriteTo ( jsonWriter ) ;
}
}
}
jsonWriter . WriteEndArray ( ) ;
}
}
private static bool TryCreateDirectory ( string path )
{
try
{
if ( ! Directory . Exists ( path ) )
{
Directory . CreateDirectory ( path ) ;
return true ;
}
}
catch ( Exception ex3 )
{
Logger . LogError ( $"There was an error in TryCreateDirectory {path}: {ex3.Message}" , ex3 ) ;
return false ;
}
return true ;
}
private static bool TryDeleteDirectory ( string path )
{
try
{
if ( Directory . Exists ( path ) )
{
Directory . Delete ( path , true ) ;
return true ;
}
}
catch ( Exception ex3 )
{
Logger . LogError ( $"There was an error in TryDeleteDirectory {path}: {ex3.Message}" , ex3 ) ;
return false ;
}
return true ;
}
/// <summary>
/// Method <c>SetRegSettingsBackupAndRestoreItem</c> helper method to write to the registry.
/// </summary>
public static void SetRegSettingsBackupAndRestoreItem ( string itemName , string itemValue )
{
using ( var key = Microsoft . Win32 . Registry . CurrentUser . OpenSubKey ( "Software\\Microsoft" , true ) )
{
var ptKey = key . OpenSubKey ( "PowerToys" , true ) ;
if ( ptKey ! = null )
{
ptKey . SetValue ( itemName , itemValue ) ;
}
else
{
var newPtKey = key . CreateSubKey ( "PowerToys" ) ;
newPtKey . SetValue ( itemName , itemValue ) ;
}
}
}
/// <summary>
/// Method <c>GetRegSettingsBackupAndRestoreRegItem</c> helper method to read from the registry.
/// </summary>
public static string GetRegSettingsBackupAndRestoreRegItem ( string itemName )
{
using ( var key = Microsoft . Win32 . Registry . CurrentUser . OpenSubKey ( "Software\\Microsoft\\PowerToys" ) )
{
if ( key ! = null )
{
var val = key . GetValue ( itemName ) ;
if ( val ! = null )
{
return val . ToString ( ) ;
}
}
}
return null ;
}
/// <summary>
/// Method <c>RestoreSettings</c> returns a folder that has the latest backup in it.
/// </summary>
/// <returns>
/// A tuple that indicates if the backup was done or not, and a message.
/// The message usually is a localized reference key.
/// </returns>
2022-12-18 14:27:14 +01:00
public ( bool Success , string Message , string Severity ) RestoreSettings ( string appBasePath , string settingsBackupAndRestoreDir )
2022-10-13 03:41:21 -04:00
{
try
{
// verify inputs
if ( ! Directory . Exists ( appBasePath ) )
{
return ( false , $"Invalid appBasePath {appBasePath}" , "Error" ) ;
}
if ( string . IsNullOrEmpty ( settingsBackupAndRestoreDir ) )
{
return ( false , $"General_SettingsBackupAndRestore_NoBackupSyncPath" , "Error" ) ;
}
if ( ! Directory . Exists ( settingsBackupAndRestoreDir ) )
{
Logger . LogError ( $"Invalid settingsBackupAndRestoreDir {settingsBackupAndRestoreDir}" ) ;
return ( false , "General_SettingsBackupAndRestore_InvalidBackupLocation" , "Error" ) ;
}
var latestSettingsFolder = GetLatestSettingsFolder ( ) ;
if ( latestSettingsFolder = = null )
{
return ( false , $"General_SettingsBackupAndRestore_NoBackupsFound" , "Warning" ) ;
}
// get data needed for process
2023-10-04 11:13:01 +01:00
var backupRestoreSettings = JsonNode . Parse ( GetBackupRestoreSettingsJson ( ) ) ;
var currentSettingsFiles = GetSettingsFiles ( backupRestoreSettings , appBasePath ) . ToList ( ) . ToDictionary ( x = > x . Substring ( appBasePath . Length ) ) ;
var backupSettingsFiles = GetSettingsFiles ( backupRestoreSettings , latestSettingsFolder ) . ToList ( ) . ToDictionary ( x = > x . Substring ( latestSettingsFolder . Length ) ) ;
2022-10-13 03:41:21 -04:00
if ( backupSettingsFiles . Count = = 0 )
{
return ( false , $"General_SettingsBackupAndRestore_NoBackupsFound" , "Warning" ) ;
}
var anyFilesUpdated = false ;
foreach ( var currentFile in backupSettingsFiles )
{
var relativePath = currentFile . Value . Substring ( latestSettingsFolder . Length + 1 ) ;
2023-10-04 11:13:01 +01:00
var restoreFullPath = Path . Combine ( appBasePath , relativePath ) ;
var settingsToRestoreJson = GetExportVersion ( backupRestoreSettings , currentFile . Key , currentFile . Value ) ;
2022-10-13 03:41:21 -04:00
2023-03-16 15:51:31 +01:00
if ( currentSettingsFiles . TryGetValue ( currentFile . Key , out string value ) )
2022-10-13 03:41:21 -04:00
{
// we have a setting file to restore to
2023-10-04 11:13:01 +01:00
var currentSettingsFileJson = GetExportVersion ( backupRestoreSettings , currentFile . Key , value ) ;
2022-10-13 03:41:21 -04:00
if ( JsonNormalizer . Normalize ( settingsToRestoreJson ) ! = JsonNormalizer . Normalize ( currentSettingsFileJson ) )
{
// the settings file needs to be updated, update the real one with non-excluded stuff...
Logger . LogInfo ( $"Settings file {currentFile.Key} is different and is getting updated from backup" ) ;
[KBM]Launch apps / URI with keyboard shortcuts, support chords (#30121)
* Working UI update with just runProgram Path and isRunProgram
* First working, basic. no args or path, or setting change detections.
* Revert and fixed.
* Some clean up, working with config file monitor
* Args and Start-in should be working.
* File monitor, quotes, xaml screens one
* Fixed enable/disable toogle from XAML
* Code cleanup.
* Betting logging.
* Cleanup, start of RunProgramDescriptor and usage of run_non_elevated/run_elevated
* Code moved to KeyboardEventHandlers, but not enabled since it won't build as is, needs elevation.h. Other testing..
* Key chords working, pretty much
* Added gui for elevation level, need to refresh on change...
* f: include shellapi.h and reference wil in KBMEL
* run_elevated/run_non_elevated sorted out. Working!
* Removed lots of old temp code.
* Fix some speling errors.
* Cleanup before trying to add a UI for the chord
* Added "DifferentUser" option
* Closer on UI for chords.
* Better UI, lots working.
* Clean up
* Text for “Allow chords” – needs to look better…
* Bugs and clean-up
* Cleanup
* Refactor and clean up.
* More clean up
* Some localization.
* Don’t show “Allow chords“ to the “to” shortcut
* Maybe better foreground after opening new app
* Better chord matching.
* Runprogram fix for stealing existing shortcut.
* Better runProgram stuff
* Temp commit
* Working well
* Toast test
* More toast
* Added File and Folder picker UI
* Pre-check on run program file exists.
* Refactor to SetupRunProgramControls
* Open URI UI is going.
* Open URI working well
* Open URI stuff working well
* Allowed AppSpecific shortcut and fixed backup/restore shortcut dups
* Fixed settings screen
* Start of code to find by name...
* UI fixed
* Small fixes
* Some single edit code working.
* UI getting better.
* Fixes
* Fixed and merge from main
* UI updates
* UI updates.
* UI stuff
* Fixed crash from move ui item locations.
* Fixed crash from move ui item locations.
* Added delete confirm
* Basic sound working.
* Localized some stuff
* Added sounds
* Better experiance when shortcut is in use.
* UI tweaks
* Fixed KBM ui for unicode shortcut not having ","
* Some clean up
* Cleanup
* Cleanup
* Fixed applyXamlStyling
* Added back stuff lost in merge
* applyXamlStyling, again
* Fixed crash on change from non shortcut to shortcut
* Update src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj
* Fixed some spelling type issues.
* ImplementationLibrary 231216
* Comment bump to see if the Microsoft.Windows.ImplementationLibrary version thing gets picked up
* Correct, Microsoft.Windows.ImplementationLibrary, finally?
* Fixed two test that failed because we now allow key-chords.
* Removed shortcut sounds.
* use original behavior when "allow chords" is off in shortcut window
* fix crash when editing a shortcut that has apps specified for it
* split KBM chords with comma on dashboard page
* Fix some spelling items.
* More "spelling"
* Fix XAML styling
* align TextBlock and ToggleSwitch
* fix cutoff issue at the top
* increase ComboBox width
* Added *Unsupported* for backwards compat on config of KBM
* fix spellcheck
* Fix crash on Remap key screen
* Fixed Remap Keys ComboBox width too short.
* Removed KBM Single Edit mode, fixed crash.
* Fix Xaml with xaml cops
* Fix crash on setting "target app" for some types of shortcuts.
* Space to toggle chord, combobox back
* fix spellcheck
* fix some code nits
* Code review updates.
* Add exclusions to the bug report tool
* Code review and kill CloseAndEndTask
* Fix alignment / 3 comboboxes per row
* Fix daily telemetry events to exclude start app and open URI
* Add chords and remove app start and open uri from config telemetry
* comma instead of plus in human readable shortcut telemetry data
* Code review, restore default-old state when new row added in KBM
* Code review, restore default-old state when new row added in KBM, part 2
* Still show target app on Settings
* Only allow enabling chords for origin shortcuts
---------
Co-authored-by: Andrey Nekrasov <yuyoyuppe@users.noreply.github.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
2024-02-27 18:12:05 -05:00
// we needed a new "CustomRestoreSettings" for now, to overwrite because some settings don't merge well (like KBM shortcuts)
var overwrite = false ;
if ( backupRestoreSettings [ "CustomRestoreSettings" ] ! = null & & backupRestoreSettings [ "CustomRestoreSettings" ] [ currentFile . Key ] ! = null )
{
var customRestoreSettings = backupRestoreSettings [ "CustomRestoreSettings" ] [ currentFile . Key ] ;
overwrite = customRestoreSettings [ "overwrite" ] ! = null & & ( bool ) customRestoreSettings [ "overwrite" ] ;
}
if ( overwrite )
{
File . WriteAllText ( currentSettingsFiles [ currentFile . Key ] , settingsToRestoreJson ) ;
}
else
{
var newCurrentSettingsFile = JsonMergeHelper . Merge ( File . ReadAllText ( currentSettingsFiles [ currentFile . Key ] ) , settingsToRestoreJson ) ;
File . WriteAllText ( currentSettingsFiles [ currentFile . Key ] , newCurrentSettingsFile ) ;
}
2022-10-13 03:41:21 -04:00
anyFilesUpdated = true ;
}
}
else
{
// we don't have anything to merge this into, we need to use it as is
Logger . LogInfo ( $"Settings file {currentFile.Key} is in the backup but does not exist for restore" ) ;
var thisPathToRestore = Path . Combine ( appBasePath , currentFile . Key . Substring ( 1 ) ) ;
TryCreateDirectory ( Path . GetDirectoryName ( thisPathToRestore ) ) ;
File . WriteAllText ( thisPathToRestore , settingsToRestoreJson ) ;
anyFilesUpdated = true ;
}
}
if ( anyFilesUpdated )
{
// something was changed do we need to return true to indicate a restart is needed.
2023-10-04 11:13:01 +01:00
var restartAfterRestore = ( bool? ) backupRestoreSettings ! [ "RestartAfterRestore" ] ;
2022-10-13 03:41:21 -04:00
if ( ! restartAfterRestore . HasValue | | restartAfterRestore . Value )
{
return ( true , $"RESTART APP" , "Success" ) ;
}
else
{
return ( false , $"RESTART APP" , "Success" ) ;
}
}
else
{
return ( false , $"General_SettingsBackupAndRestore_NothingToRestore" , "Informational" ) ;
}
}
catch ( Exception ex2 )
{
Logger . LogError ( "Error in RestoreSettings, " + ex2 . ToString ( ) ) ;
return ( false , $"General_SettingsBackupAndRestore_BackupError" , "Error" ) ;
}
}
/// <summary>
Updates for check-spelling v0.0.25 (#40386)
## Summary of the Pull Request
- #39572 updated check-spelling but ignored:
> 🐣 Breaking Changes
[Code Scanning action requires a Code Scanning
Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset)
If you use SARIF reporting, then instead of the workflow yielding an ❌
when it fails, it will rely on [github-advanced-security
🤖](https://github.com/apps/github-advanced-security) to report the
failure. You will need to adjust your checks for PRs.
This means that check-spelling hasn't been properly doing its job 😦.
I'm sorry, I should have pushed a thing to this repo earlier,...
Anyway, as with most refreshes, this comes with a number of fixes, some
are fixes for typos that snuck in before the 0.0.25 upgrade, some are
for things that snuck in after, some are based on new rules in
spell-check-this, and some are hand written patterns based on running
through this repository a few times.
About the 🐣 **breaking change**: someone needs to create a ruleset for
this repository (see [Code Scanning action requires a Code Scanning
Ruleset: Sample ruleset
](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)).
The alternative to adding a ruleset is to change the condition to not
use sarif for this repository. In general, I think the github
integration from sarif is prettier/more helpful, so I think that it's
the better choice.
You can see an example of it working in:
- https://github.com/check-spelling-sandbox/PowerToys/pull/23
---------
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
/// Method <c>GetSettingsBackupAndRestoreDir</c> returns the path of the backup and restore location.
2022-10-13 03:41:21 -04:00
/// </summary>
/// <remarks>
2022-10-25 21:24:24 +02:00
/// This will return a default location based on user documents if non is set.
2022-10-13 03:41:21 -04:00
/// </remarks>
public string GetSettingsBackupAndRestoreDir ( )
{
string settingsBackupAndRestoreDir = GetRegSettingsBackupAndRestoreRegItem ( "SettingsBackupAndRestoreDir" ) ;
if ( settingsBackupAndRestoreDir = = null )
{
2022-10-25 21:24:24 +02:00
settingsBackupAndRestoreDir = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) , "PowerToys\\Backup" ) ;
2022-10-13 03:41:21 -04:00
}
return settingsBackupAndRestoreDir ;
}
2023-12-28 13:37:13 +03:00
private List < string > GetBackupSettingsFiles ( string settingsBackupAndRestoreDir )
2022-10-13 03:41:21 -04:00
{
return Directory . GetFiles ( settingsBackupAndRestoreDir , "settings_*.ptb" , SearchOption . TopDirectoryOnly ) . ToList ( ) . Where ( f = > Regex . IsMatch ( f , "settings_(\\d{1,19}).ptb" ) ) . ToList ( ) ;
}
/// <summary>
/// Method <c>GetLatestSettingsFolder</c> returns a folder that has the latest backup in it.
/// </summary>
/// <remarks>
/// The backup will usually be a backup file that has to be extracted to a temp folder. This will do that for us.
/// </remarks>
private string GetLatestSettingsFolder ( )
{
string settingsBackupAndRestoreDir = GetSettingsBackupAndRestoreDir ( ) ;
if ( settingsBackupAndRestoreDir = = null )
{
return null ;
}
if ( ! Directory . Exists ( settingsBackupAndRestoreDir ) )
{
return null ;
}
var settingsBackupFolders = new Dictionary < long , string > ( ) ;
var settingsBackupFiles = GetBackupSettingsFiles ( settingsBackupAndRestoreDir ) . ToDictionary ( x = > long . Parse ( Path . GetFileName ( x ) . Replace ( "settings_" , string . Empty ) . Replace ( ".ptb" , string . Empty ) , CultureInfo . InvariantCulture ) ) ;
var latestFolder = 0L ;
var latestFile = 0L ;
if ( settingsBackupFolders . Count > 0 )
{
latestFolder = settingsBackupFolders . OrderByDescending ( x = > x . Key ) . FirstOrDefault ( ) . Key ;
}
if ( settingsBackupFiles . Count > 0 )
{
latestFile = settingsBackupFiles . OrderByDescending ( x = > x . Key ) . FirstOrDefault ( ) . Key ;
}
if ( latestFile = = 0 & & latestFolder = = 0 )
{
return null ;
}
else if ( latestFolder > = latestFile )
{
return settingsBackupFolders [ latestFolder ] ;
}
else
{
var tempPath = Path . GetTempPath ( ) ;
var fullBackupDir = Path . Combine ( tempPath , "PowerToys_settings_" + latestFile . ToString ( CultureInfo . InvariantCulture ) ) ;
2022-11-15 09:54:23 -05:00
lock ( backupSettingsInternalLock )
2022-10-13 03:41:21 -04:00
{
2022-11-15 09:54:23 -05:00
if ( ! Directory . Exists ( fullBackupDir ) | | ! File . Exists ( Path . Combine ( fullBackupDir , "manifest.json" ) ) )
{
TryDeleteDirectory ( fullBackupDir ) ;
ZipFile . ExtractToDirectory ( settingsBackupFiles [ latestFile ] , fullBackupDir ) ;
}
2022-10-13 03:41:21 -04:00
}
ThreadPool . QueueUserWorkItem ( ( x ) = >
{
try
{
RemoveOldBackups ( tempPath , 1 , TimeSpan . FromDays ( 7 ) ) ;
}
catch
{
// hmm, ok
}
} ) ;
return fullBackupDir ;
}
}
/// <summary>
/// Method <c>GetLatestBackupFileName</c> returns the name of the newest backup file.
/// </summary>
public string GetLatestBackupFileName ( )
{
string settingsBackupAndRestoreDir = GetSettingsBackupAndRestoreDir ( ) ;
if ( string . IsNullOrEmpty ( settingsBackupAndRestoreDir ) | | ! Directory . Exists ( settingsBackupAndRestoreDir ) )
{
return string . Empty ;
}
var settingsBackupFiles = GetBackupSettingsFiles ( settingsBackupAndRestoreDir ) ;
if ( settingsBackupFiles . Count > 0 )
{
return Path . GetFileName ( settingsBackupFiles . OrderByDescending ( x = > x ) . First ( ) ) ;
}
else
{
return string . Empty ;
}
}
/// <summary>
/// Method <c>GetLatestSettingsBackupManifest</c> get's the meta data from a backup file.
/// </summary>
public JsonNode GetLatestSettingsBackupManifest ( )
{
var folder = GetLatestSettingsFolder ( ) ;
if ( folder = = null )
{
return null ;
}
return JsonNode . Parse ( File . ReadAllText ( Path . Combine ( folder , "manifest.json" ) ) ) ;
}
/// <summary>
/// Method <c>IsIncludeFile</c> check's to see if a settings file is to be included during backup and restore.
/// </summary>
private static bool IsIncludeFile ( JsonNode settings , string name )
{
foreach ( var test in ( JsonArray ) settings [ "IncludeFiles" ] )
{
if ( Regex . IsMatch ( name , WildCardToRegular ( test . ToString ( ) ) ) )
{
return true ;
}
}
return false ;
}
/// <summary>
/// Method <c>IsIgnoreFile</c> check's to see if a settings file is to be ignored during backup and restore.
/// </summary>
private static bool IsIgnoreFile ( JsonNode settings , string name )
{
foreach ( var test in ( JsonArray ) settings [ "IgnoreFiles" ] )
{
if ( Regex . IsMatch ( name , WildCardToRegular ( test . ToString ( ) ) ) )
{
return true ;
}
}
return false ;
}
/// <summary>
/// Class <c>GetSettingsFiles</c> returns the effective list of settings files.
/// </summary>
/// <remarks>
/// Handles all the included/exclude files.
/// </remarks>
private static string [ ] GetSettingsFiles ( JsonNode settings , string path )
{
if ( string . IsNullOrEmpty ( path ) | | ! Directory . Exists ( path ) )
{
return Array . Empty < string > ( ) ;
}
return Directory . GetFiles ( path , "*.json" , SearchOption . AllDirectories ) . Where ( s = > IsIncludeFile ( settings , s ) & & ! IsIgnoreFile ( settings , s ) ) . ToArray ( ) ;
}
/// <summary>
/// Method <c>BackupSettings</c> does the backup process.
/// </summary>
/// <returns>
/// A tuple that indicates if the backup was done or not, and a message.
/// The message usually is a localized reference key.
/// </returns>
/// <remarks>
/// This is a wrapper for BackupSettingsInternal, so we can check the time to run.
/// </remarks>
2023-06-14 12:56:56 +03:00
public ( bool Success , string Message , string Severity , bool LastBackupExists , string OptionalMessage ) BackupSettings ( string appBasePath , string settingsBackupAndRestoreDir , bool dryRun )
2022-10-13 03:41:21 -04:00
{
var sw = Stopwatch . StartNew ( ) ;
var results = BackupSettingsInternal ( appBasePath , settingsBackupAndRestoreDir , dryRun ) ;
sw . Stop ( ) ;
Logger . LogInfo ( $"BackupSettings took {sw.ElapsedMilliseconds}" ) ;
2022-12-18 14:27:14 +01:00
lastBackupSettingsResults = ( results . Success , results . Severity , results . LastBackupExists , DateTime . UtcNow ) ;
2022-10-13 03:41:21 -04:00
return results ;
}
/// <summary>
/// Method <c>DryRunBackup</c> wrapper function to do a dry-run backup
/// </summary>
2023-06-14 12:56:56 +03:00
public ( bool Success , string Message , string Severity , bool LastBackupExists , string OptionalMessage ) DryRunBackup ( )
2022-10-13 03:41:21 -04:00
{
var settingsUtils = new SettingsUtils ( ) ;
var appBasePath = Path . GetDirectoryName ( settingsUtils . GetSettingsFilePath ( ) ) ;
string settingsBackupAndRestoreDir = GetSettingsBackupAndRestoreDir ( ) ;
var results = BackupSettings ( appBasePath , settingsBackupAndRestoreDir , true ) ;
2022-12-18 14:27:14 +01:00
lastBackupSettingsResults = ( results . Success , results . Severity , results . LastBackupExists , DateTime . UtcNow ) ;
2022-10-13 03:41:21 -04:00
return results ;
}
/// <summary>
/// Method <c>GetLastBackupSettingsResults</c> gets the results from the last backup process
/// </summary>
/// <returns>
/// A tuple that indicates if the backup was done or not, and other information
/// </returns>
2022-12-18 14:27:14 +01:00
public ( bool Success , bool HadError , bool LastBackupExists , DateTime ? LastRan ) GetLastBackupSettingsResults ( )
2022-10-13 03:41:21 -04:00
{
2022-12-18 14:27:14 +01:00
return ( lastBackupSettingsResults . Success , lastBackupSettingsResults . Severity = = "Error" , lastBackupSettingsResults . LastBackupExists , lastBackupSettingsResults . LastRan ) ;
2022-10-13 03:41:21 -04:00
}
/// <summary>
/// Method <c>BackupSettingsInternal</c> does the backup process.
/// </summary>
/// <returns>
/// A tuple that indicates if the backup was done or not, and a message.
/// The message usually is a localized reference key.
/// </returns>
2023-06-14 12:56:56 +03:00
private ( bool Success , string Message , string Severity , bool LastBackupExists , string OptionalMessage ) BackupSettingsInternal ( string appBasePath , string settingsBackupAndRestoreDir , bool dryRun )
2022-10-13 03:41:21 -04:00
{
var lastBackupExists = false ;
lock ( backupSettingsInternalLock )
{
// simulated delay to validate behavior
// Thread.Sleep(1000);
2023-06-14 12:56:56 +03:00
KeyValuePair < string , string > tempFile = default ( KeyValuePair < string , string > ) ;
2022-10-13 03:41:21 -04:00
try
{
// verify inputs
if ( ! Directory . Exists ( appBasePath ) )
{
2023-06-14 12:56:56 +03:00
return ( false , $"Invalid appBasePath {appBasePath}" , "Error" , lastBackupExists , string . Empty ) ;
2022-10-13 03:41:21 -04:00
}
if ( string . IsNullOrEmpty ( settingsBackupAndRestoreDir ) )
{
2023-06-14 12:56:56 +03:00
return ( false , $"General_SettingsBackupAndRestore_NoBackupSyncPath" , "Error" , lastBackupExists , "\n" + settingsBackupAndRestoreDir ) ;
2022-10-13 03:41:21 -04:00
}
if ( ! Path . IsPathRooted ( settingsBackupAndRestoreDir ) )
{
2023-06-14 12:56:56 +03:00
return ( false , $"Invalid settingsBackupAndRestoreDir, not rooted" , "Error" , lastBackupExists , "\n" + settingsBackupAndRestoreDir ) ;
2022-10-13 03:41:21 -04:00
}
if ( settingsBackupAndRestoreDir . StartsWith ( appBasePath , StringComparison . InvariantCultureIgnoreCase ) )
{
// backup cannot be under app
Logger . LogError ( $"BackupSettings, backup cannot be under app" ) ;
2023-06-14 12:56:56 +03:00
return ( false , "General_SettingsBackupAndRestore_InvalidBackupLocation" , "Error" , lastBackupExists , "\n" + appBasePath ) ;
2022-10-13 03:41:21 -04:00
}
Fix: Prevent backup directory creation during dry runs (#41460)
## Summary
This PR fixes an issue where the backup folder was being created
unnecessarily when users navigated to the General tab in PowerToys
Settings, even when no actual backup had been triggered.
## Problem
When opening PowerToys Settings and navigating to the General tab, the
backup directory (default: `~/Documents/PowerToys/Backup`) was
automatically created, even though no backup operation had been
performed. This caused confusion for users setting up PowerToys on new
devices, as they would always need to manually clean up the unwanted
default folder when configuring a custom backup path.
## Root Cause
The issue occurred because:
1. Loading the General tab triggers `RefreshBackupRestoreStatus()`
2. This calls `DryRunBackup()` to check backup status
3. `DryRunBackup()` executes `BackupSettingsInternal()` with
`dryRun=true`
4. However, the directory creation logic (`TryCreateDirectory`) was
running regardless of the dry run flag
## Solution
The fix ensures that directory creation only happens during actual
backup operations:
**Primary Change**: Wrapped backup directory creation in a dry run
check:
```csharp
// Only create the backup directory if this is not a dry run
if (!dryRun)
{
var dirExists = TryCreateDirectory(settingsBackupAndRestoreDir);
if (!dirExists)
{
Logger.LogError($"Failed to create dir {settingsBackupAndRestoreDir}");
return (false, $"General_SettingsBackupAndRestore_BackupError", "Error", lastBackupExists, "\n" + settingsBackupAndRestoreDir);
}
}
```
**Consistency Change**: Also moved temporary directory creation inside
dry run checks to maintain consistent behavior throughout the backup
process.
## Impact
- ✅ **General tab loading**: No longer creates unwanted backup
directories
- ✅ **Actual backup functionality**: Remains completely unchanged
- ✅ **User experience**: Clean setup without unwanted default folders
- ✅ **No breaking changes**: All existing backup/restore features work
as before
## Testing
Created comprehensive tests to validate:
- Dry runs (General tab loading) don't create directories
- Actual backup operations create directories as expected
- No regression in existing backup/restore functionality
Fixes #38620.
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `i1qvsblobprodcus353.vsblob.vsassets.io`
> - Triggering command: `dotnet build
src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj -c Debug
--nologo` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for
you](https://github.com/microsoft/PowerToys/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-09-29 09:11:28 +08:00
// Only create the backup directory if this is not a dry run
if ( ! dryRun )
2022-10-13 03:41:21 -04:00
{
Fix: Prevent backup directory creation during dry runs (#41460)
## Summary
This PR fixes an issue where the backup folder was being created
unnecessarily when users navigated to the General tab in PowerToys
Settings, even when no actual backup had been triggered.
## Problem
When opening PowerToys Settings and navigating to the General tab, the
backup directory (default: `~/Documents/PowerToys/Backup`) was
automatically created, even though no backup operation had been
performed. This caused confusion for users setting up PowerToys on new
devices, as they would always need to manually clean up the unwanted
default folder when configuring a custom backup path.
## Root Cause
The issue occurred because:
1. Loading the General tab triggers `RefreshBackupRestoreStatus()`
2. This calls `DryRunBackup()` to check backup status
3. `DryRunBackup()` executes `BackupSettingsInternal()` with
`dryRun=true`
4. However, the directory creation logic (`TryCreateDirectory`) was
running regardless of the dry run flag
## Solution
The fix ensures that directory creation only happens during actual
backup operations:
**Primary Change**: Wrapped backup directory creation in a dry run
check:
```csharp
// Only create the backup directory if this is not a dry run
if (!dryRun)
{
var dirExists = TryCreateDirectory(settingsBackupAndRestoreDir);
if (!dirExists)
{
Logger.LogError($"Failed to create dir {settingsBackupAndRestoreDir}");
return (false, $"General_SettingsBackupAndRestore_BackupError", "Error", lastBackupExists, "\n" + settingsBackupAndRestoreDir);
}
}
```
**Consistency Change**: Also moved temporary directory creation inside
dry run checks to maintain consistent behavior throughout the backup
process.
## Impact
- ✅ **General tab loading**: No longer creates unwanted backup
directories
- ✅ **Actual backup functionality**: Remains completely unchanged
- ✅ **User experience**: Clean setup without unwanted default folders
- ✅ **No breaking changes**: All existing backup/restore features work
as before
## Testing
Created comprehensive tests to validate:
- Dry runs (General tab loading) don't create directories
- Actual backup operations create directories as expected
- No regression in existing backup/restore functionality
Fixes #38620.
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `i1qvsblobprodcus353.vsblob.vsassets.io`
> - Triggering command: `dotnet build
src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj -c Debug
--nologo` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for
you](https://github.com/microsoft/PowerToys/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-09-29 09:11:28 +08:00
var dirExists = TryCreateDirectory ( settingsBackupAndRestoreDir ) ;
if ( ! dirExists )
{
Logger . LogError ( $"Failed to create dir {settingsBackupAndRestoreDir}" ) ;
return ( false , $"General_SettingsBackupAndRestore_BackupError" , "Error" , lastBackupExists , "\n" + settingsBackupAndRestoreDir ) ;
}
2022-10-13 03:41:21 -04:00
}
// get data needed for process
2023-10-04 11:13:01 +01:00
var backupRestoreSettings = JsonNode . Parse ( GetBackupRestoreSettingsJson ( ) ) ;
var currentSettingsFiles = GetSettingsFiles ( backupRestoreSettings , appBasePath ) . ToList ( ) . ToDictionary ( x = > x . Substring ( appBasePath . Length ) ) ;
2022-10-13 03:41:21 -04:00
var fullBackupDir = Path . Combine ( Path . GetTempPath ( ) , $"settings_{DateTime.UtcNow.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture)}" ) ;
var latestSettingsFolder = GetLatestSettingsFolder ( ) ;
2023-10-04 11:13:01 +01:00
var lastBackupSettingsFiles = GetSettingsFiles ( backupRestoreSettings , latestSettingsFolder ) . ToList ( ) . ToDictionary ( x = > x . Substring ( latestSettingsFolder . Length ) ) ;
2022-10-13 03:41:21 -04:00
lastBackupExists = lastBackupSettingsFiles . Count > 0 ;
if ( currentSettingsFiles . Count = = 0 )
{
2023-06-14 12:56:56 +03:00
return ( false , "General_SettingsBackupAndRestore_NoSettingsFilesFound" , "Error" , lastBackupExists , string . Empty ) ;
2022-10-13 03:41:21 -04:00
}
var anyFileBackedUp = false ;
2022-12-18 14:27:14 +01:00
var skippedSettingsFiles = new Dictionary < string , ( string Path , string Settings ) > ( ) ;
2022-10-13 03:41:21 -04:00
var updatedSettingsFiles = new Dictionary < string , string > ( ) ;
foreach ( var currentFile in currentSettingsFiles )
{
2023-06-14 12:56:56 +03:00
tempFile = currentFile ;
2022-10-13 03:41:21 -04:00
// need to check and back this up;
2023-10-04 11:13:01 +01:00
var currentSettingsFileToBackup = GetExportVersion ( backupRestoreSettings , currentFile . Key , currentFile . Value ) ;
2022-10-13 03:41:21 -04:00
var doBackup = false ;
2023-03-16 15:51:31 +01:00
if ( lastBackupSettingsFiles . TryGetValue ( currentFile . Key , out string value ) )
2022-10-13 03:41:21 -04:00
{
// there is a previous backup for this, get an export version of it.
2023-10-04 11:13:01 +01:00
var lastSettingsFileDoc = GetExportVersion ( backupRestoreSettings , currentFile . Key , value ) ;
2022-10-13 03:41:21 -04:00
// check to see if the new export version would be same as last export version.
if ( JsonNormalizer . Normalize ( currentSettingsFileToBackup ) ! = JsonNormalizer . Normalize ( lastSettingsFileDoc ) )
{
doBackup = true ;
Logger . LogInfo ( $"BackupSettings, {currentFile.Value} content is different." ) ;
}
}
else
{
// this has never been backed up, we need to do it now.
Logger . LogInfo ( $"BackupSettings, {currentFile.Value} does not exists." ) ;
doBackup = true ;
}
if ( doBackup )
{
// add to list of files we noted as needing backup
updatedSettingsFiles . Add ( currentFile . Key , currentFile . Value ) ;
// mark overall flag that a backup will be made
anyFileBackedUp = true ;
// write the export version of the settings file to backup location.
var relativePath = currentFile . Value . Substring ( appBasePath . Length + 1 ) ;
var backupFullPath = Path . Combine ( fullBackupDir , relativePath ) ;
Logger . LogInfo ( $"BackupSettings writing, {backupFullPath}, dryRun:{dryRun}." ) ;
if ( ! dryRun )
{
Fix: Prevent backup directory creation during dry runs (#41460)
## Summary
This PR fixes an issue where the backup folder was being created
unnecessarily when users navigated to the General tab in PowerToys
Settings, even when no actual backup had been triggered.
## Problem
When opening PowerToys Settings and navigating to the General tab, the
backup directory (default: `~/Documents/PowerToys/Backup`) was
automatically created, even though no backup operation had been
performed. This caused confusion for users setting up PowerToys on new
devices, as they would always need to manually clean up the unwanted
default folder when configuring a custom backup path.
## Root Cause
The issue occurred because:
1. Loading the General tab triggers `RefreshBackupRestoreStatus()`
2. This calls `DryRunBackup()` to check backup status
3. `DryRunBackup()` executes `BackupSettingsInternal()` with
`dryRun=true`
4. However, the directory creation logic (`TryCreateDirectory`) was
running regardless of the dry run flag
## Solution
The fix ensures that directory creation only happens during actual
backup operations:
**Primary Change**: Wrapped backup directory creation in a dry run
check:
```csharp
// Only create the backup directory if this is not a dry run
if (!dryRun)
{
var dirExists = TryCreateDirectory(settingsBackupAndRestoreDir);
if (!dirExists)
{
Logger.LogError($"Failed to create dir {settingsBackupAndRestoreDir}");
return (false, $"General_SettingsBackupAndRestore_BackupError", "Error", lastBackupExists, "\n" + settingsBackupAndRestoreDir);
}
}
```
**Consistency Change**: Also moved temporary directory creation inside
dry run checks to maintain consistent behavior throughout the backup
process.
## Impact
- ✅ **General tab loading**: No longer creates unwanted backup
directories
- ✅ **Actual backup functionality**: Remains completely unchanged
- ✅ **User experience**: Clean setup without unwanted default folders
- ✅ **No breaking changes**: All existing backup/restore features work
as before
## Testing
Created comprehensive tests to validate:
- Dry runs (General tab loading) don't create directories
- Actual backup operations create directories as expected
- No regression in existing backup/restore functionality
Fixes #38620.
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `i1qvsblobprodcus353.vsblob.vsassets.io`
> - Triggering command: `dotnet build
src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj -c Debug
--nologo` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for
you](https://github.com/microsoft/PowerToys/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-09-29 09:11:28 +08:00
TryCreateDirectory ( fullBackupDir ) ;
TryCreateDirectory ( Path . GetDirectoryName ( backupFullPath ) ) ;
2022-10-13 03:41:21 -04:00
File . WriteAllText ( backupFullPath , currentSettingsFileToBackup ) ;
}
}
else
{
// if we found no reason to backup this settings file, record that in this collection
skippedSettingsFiles . Add ( currentFile . Key , ( currentFile . Value , currentSettingsFileToBackup ) ) ;
}
}
if ( ! anyFileBackedUp )
{
// nothing was done!
2023-06-14 12:56:56 +03:00
return ( false , $"General_SettingsBackupAndRestore_NothingToBackup" , "Informational" , lastBackupExists , "\n" + tempFile . Value ) ;
2022-10-13 03:41:21 -04:00
}
// add skipped.
foreach ( var currentFile in skippedSettingsFiles )
{
// if we did do a backup, we need to copy in all the settings files we skipped so the backup is complete.
// this is needed since we might use the backup on another machine/
2022-12-18 14:27:14 +01:00
var relativePath = currentFile . Value . Path . Substring ( appBasePath . Length + 1 ) ;
2022-10-13 03:41:21 -04:00
var backupFullPath = Path . Combine ( fullBackupDir , relativePath ) ;
Logger . LogInfo ( $"BackupSettings writing, {backupFullPath}, dryRun:{dryRun}" ) ;
if ( ! dryRun )
{
TryCreateDirectory ( fullBackupDir ) ;
TryCreateDirectory ( Path . GetDirectoryName ( backupFullPath ) ) ;
2022-12-18 14:27:14 +01:00
File . WriteAllText ( backupFullPath , currentFile . Value . Settings ) ;
2022-10-13 03:41:21 -04:00
}
}
// add manifest
var manifestData = new
{
CreateDateTime = DateTime . UtcNow . ToString ( "u" , CultureInfo . InvariantCulture ) ,
@Version = Helper . GetProductVersion ( ) ,
UpdatedFiles = updatedSettingsFiles . Keys . ToList ( ) ,
BackupSource = Environment . MachineName ,
UnchangedFiles = skippedSettingsFiles . Keys . ToList ( ) ,
} ;
2023-12-28 13:37:13 +03:00
var manifest = JsonSerializer . Serialize ( manifestData , _serializerOptions ) ;
2022-10-13 03:41:21 -04:00
if ( ! dryRun )
{
File . WriteAllText ( Path . Combine ( fullBackupDir , "manifest.json" ) , manifest ) ;
// clean up, to prevent runaway disk usage.
RemoveOldBackups ( settingsBackupAndRestoreDir , 10 , TimeSpan . FromDays ( 60 ) ) ;
// compress the backup
var zipName = Path . Combine ( settingsBackupAndRestoreDir , Path . GetFileName ( fullBackupDir ) + ".ptb" ) ;
ZipFile . CreateFromDirectory ( fullBackupDir , zipName ) ;
TryDeleteDirectory ( fullBackupDir ) ;
}
2023-06-14 12:56:56 +03:00
return ( true , $"General_SettingsBackupAndRestore_BackupComplete" , "Success" , lastBackupExists , string . Empty ) ;
2022-10-13 03:41:21 -04:00
}
catch ( Exception ex2 )
{
2023-06-14 12:56:56 +03:00
Logger . LogError ( $"There was an error in {tempFile.Value} : {ex2.Message}" , ex2 ) ;
return ( false , $"General_SettingsBackupAndRestore_SettingsFormatError" , "Error" , lastBackupExists , "\n" + tempFile . Value ) ;
2022-10-13 03:41:21 -04:00
}
}
}
/// <summary>
/// Searches for the config file (Json) in two possible paths and returns its content.
/// </summary>
/// <returns>Returns the content of the config file (Json) as string.</returns>
/// <exception cref="FileNotFoundException">Thrown if file is not found.</exception>
/// <remarks>If the settings window is launched from an installed instance of PT we need the path "...\Settings\\backup_restore_settings.json" and if the settings window is launched from a local VS build of PT we need the path "...\backup_restore_settings.json".</remarks>
private static string GetBackupRestoreSettingsJson ( )
{
if ( File . Exists ( "backup_restore_settings.json" ) )
{
return File . ReadAllText ( "backup_restore_settings.json" ) ;
}
else if ( File . Exists ( "Settings\\backup_restore_settings.json" ) )
{
return File . ReadAllText ( "Settings\\backup_restore_settings.json" ) ;
}
else
{
throw new FileNotFoundException ( $"The backup_restore_settings.json could not be found at {Environment.CurrentDirectory}" ) ;
}
}
/// <summary>
/// Method <c>WildCardToRegular</c> is so we can use 'normal' wildcard syntax and instead of regex
/// </summary>
private static string WildCardToRegular ( string value )
{
return "^" + Regex . Escape ( value ) . Replace ( "\\*" , ".*" ) + "$" ;
}
/// <summary>
/// Method <c>GetExportVersion</c> gets the version of the settings file that we want to backup.
/// It will be formatted and all problematic settings removed from it.
/// </summary>
2023-10-04 11:13:01 +01:00
public static string GetExportVersion ( JsonNode backupRestoreSettings , string settingFileKey , string settingsFileName )
2022-10-13 03:41:21 -04:00
{
2023-10-04 11:13:01 +01:00
var ignoredSettings = GetIgnoredSettings ( backupRestoreSettings , settingFileKey ) ;
2022-10-13 03:41:21 -04:00
var settingsFile = JsonDocument . Parse ( File . ReadAllText ( settingsFileName ) ) ;
var outputBuffer = new ArrayBufferWriter < byte > ( ) ;
using ( var jsonWriter = new Utf8JsonWriter ( outputBuffer , new JsonWriterOptions { Indented = true } ) )
{
jsonWriter . WriteStartObject ( ) ;
foreach ( var property in settingsFile . RootElement . EnumerateObject ( ) . OrderBy ( p = > p . Name ) )
{
if ( ! ignoredSettings . Contains ( property . Name ) )
{
property . WriteTo ( jsonWriter ) ;
}
}
jsonWriter . WriteEndObject ( ) ;
}
if ( settingFileKey . Equals ( "\\PowerToys Run\\settings.json" , StringComparison . OrdinalIgnoreCase ) )
{
// PowerToys Run hack fix-up
2023-10-04 11:13:01 +01:00
var ptRunIgnoredSettings = GetPTRunIgnoredSettings ( backupRestoreSettings ) ;
2022-10-13 03:41:21 -04:00
var ptrSettings = JsonNode . Parse ( Encoding . UTF8 . GetString ( outputBuffer . WrittenSpan ) ) ;
foreach ( JsonObject pluginToChange in ptRunIgnoredSettings )
{
foreach ( JsonObject plugin in ( JsonArray ) ptrSettings [ "plugins" ] )
{
if ( plugin [ "Id" ] . ToString ( ) = = pluginToChange [ "Id" ] . ToString ( ) )
{
foreach ( var nameOfPropertyToRemove in ( JsonArray ) pluginToChange [ "Names" ] )
{
plugin . Remove ( nameOfPropertyToRemove . ToString ( ) ) ;
}
}
}
}
return ptrSettings . ToJsonString ( new JsonSerializerOptions { WriteIndented = true } ) ;
}
else
{
return JsonNode . Parse ( Encoding . UTF8 . GetString ( outputBuffer . WrittenSpan ) ) . ToJsonString ( new JsonSerializerOptions { WriteIndented = true } ) ;
}
}
/// <summary>
/// Method <c>GetPTRunIgnoredSettings</c> gets the 'Run-Plugin-level' settings we should ignore because they are problematic to backup/restore.
/// </summary>
2023-10-04 11:13:01 +01:00
private static JsonArray GetPTRunIgnoredSettings ( JsonNode backupRestoreSettings )
2022-10-13 03:41:21 -04:00
{
2023-11-22 12:46:59 -05:00
ArgumentNullException . ThrowIfNull ( backupRestoreSettings ) ;
2022-10-13 03:41:21 -04:00
2023-10-04 11:13:01 +01:00
if ( backupRestoreSettings [ "IgnoredPTRunSettings" ] ! = null )
2022-10-13 03:41:21 -04:00
{
2023-10-04 11:13:01 +01:00
return ( JsonArray ) backupRestoreSettings [ "IgnoredPTRunSettings" ] ;
2022-10-13 03:41:21 -04:00
}
return new JsonArray ( ) ;
}
/// <summary>
/// Method <c>GetIgnoredSettings</c> gets the 'top-level' settings we should ignore because they are problematic to backup/restore.
/// </summary>
2023-10-04 11:13:01 +01:00
private static string [ ] GetIgnoredSettings ( JsonNode backupRestoreSettings , string settingFileKey )
2022-10-13 03:41:21 -04:00
{
2023-11-22 12:46:59 -05:00
ArgumentNullException . ThrowIfNull ( backupRestoreSettings ) ;
2022-10-13 03:41:21 -04:00
if ( settingFileKey . StartsWith ( "\\" , StringComparison . OrdinalIgnoreCase ) )
{
settingFileKey = settingFileKey . Substring ( 1 ) ;
}
2023-10-04 11:13:01 +01:00
if ( backupRestoreSettings [ "IgnoredSettings" ] ! = null )
2022-10-13 03:41:21 -04:00
{
2023-10-04 11:13:01 +01:00
if ( backupRestoreSettings [ "IgnoredSettings" ] [ settingFileKey ] ! = null )
2022-10-13 03:41:21 -04:00
{
2023-10-04 11:13:01 +01:00
var settingsArray = ( JsonArray ) backupRestoreSettings [ "IgnoredSettings" ] [ settingFileKey ] ;
2022-10-13 03:41:21 -04:00
Console . WriteLine ( "settingsArray " + settingsArray . GetType ( ) . FullName ) ;
var settingsList = new List < string > ( ) ;
foreach ( var setting in settingsArray )
{
settingsList . Add ( setting . ToString ( ) ) ;
}
return settingsList . ToArray ( ) ;
}
else
{
return Array . Empty < string > ( ) ;
}
}
return Array . Empty < string > ( ) ;
}
/// <summary>
/// Method <c>RemoveOldBackups</c> is a helper that prevents is from having some runaway disk usages.
/// </summary>
private static void RemoveOldBackups ( string location , int minNumberToKeep , TimeSpan deleteIfOlderThanTs )
{
2024-11-13 12:36:45 -05:00
if ( ! removeOldBackupsLock . TryEnter ( 1000 ) )
2022-10-13 03:41:21 -04:00
{
return ;
}
try
{
DateTime deleteIfOlder = DateTime . UtcNow . Subtract ( deleteIfOlderThanTs ) ;
var settingsBackupFolders = Directory . GetDirectories ( location , "settings_*" , SearchOption . TopDirectoryOnly ) . ToList ( ) . Where ( f = > Regex . IsMatch ( f , "settings_(\\d{1,19})" ) ) . ToDictionary ( x = > long . Parse ( Path . GetFileName ( x ) . Replace ( "settings_" , string . Empty ) , CultureInfo . InvariantCulture ) ) . ToList ( ) ;
settingsBackupFolders . AddRange ( Directory . GetDirectories ( location , "PowerToys_settings_*" , SearchOption . TopDirectoryOnly ) . ToList ( ) . Where ( f = > Regex . IsMatch ( f , "PowerToys_settings_(\\d{1,19})" ) ) . ToDictionary ( x = > long . Parse ( Path . GetFileName ( x ) . Replace ( "PowerToys_settings_" , string . Empty ) , CultureInfo . InvariantCulture ) ) ) ;
var settingsBackupFiles = Directory . GetFiles ( location , "settings_*.ptb" , SearchOption . TopDirectoryOnly ) . ToList ( ) . Where ( f = > Regex . IsMatch ( f , "settings_(\\d{1,19}).ptb" ) ) . ToDictionary ( x = > long . Parse ( Path . GetFileName ( x ) . Replace ( "settings_" , string . Empty ) . Replace ( ".ptb" , string . Empty ) , CultureInfo . InvariantCulture ) ) ;
if ( settingsBackupFolders . Count + settingsBackupFiles . Count < = minNumberToKeep )
{
return ;
}
foreach ( var item in settingsBackupFolders )
{
var backupTime = DateTime . FromFileTimeUtc ( item . Key ) ;
if ( item . Value . Contains ( "PowerToys_settings_" , StringComparison . OrdinalIgnoreCase ) )
{
Updates for check-spelling v0.0.25 (#40386)
## Summary of the Pull Request
- #39572 updated check-spelling but ignored:
> 🐣 Breaking Changes
[Code Scanning action requires a Code Scanning
Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset)
If you use SARIF reporting, then instead of the workflow yielding an ❌
when it fails, it will rely on [github-advanced-security
🤖](https://github.com/apps/github-advanced-security) to report the
failure. You will need to adjust your checks for PRs.
This means that check-spelling hasn't been properly doing its job 😦.
I'm sorry, I should have pushed a thing to this repo earlier,...
Anyway, as with most refreshes, this comes with a number of fixes, some
are fixes for typos that snuck in before the 0.0.25 upgrade, some are
for things that snuck in after, some are based on new rules in
spell-check-this, and some are hand written patterns based on running
through this repository a few times.
About the 🐣 **breaking change**: someone needs to create a ruleset for
this repository (see [Code Scanning action requires a Code Scanning
Ruleset: Sample ruleset
](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)).
The alternative to adding a ruleset is to change the condition to not
use sarif for this repository. In general, I think the github
integration from sarif is prettier/more helpful, so I think that it's
the better choice.
You can see an example of it working in:
- https://github.com/check-spelling-sandbox/PowerToys/pull/23
---------
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
// this is a temp backup and we want to clean based on the time it was created in the temp place, not the time that the backup was made.
2022-10-13 03:41:21 -04:00
var folderCreatedTime = new DirectoryInfo ( item . Value ) . CreationTimeUtc ;
if ( folderCreatedTime > backupTime )
{
backupTime = folderCreatedTime ;
}
}
if ( backupTime < deleteIfOlder )
{
try
{
Logger . LogInfo ( $"RemoveOldBackups killing {item.Value}" ) ;
Directory . Delete ( item . Value , true ) ;
}
catch ( Exception ex2 )
{
Logger . LogError ( $"Failed to remove a setting backup folder ({item.Value}), because: ({ex2.Message})" ) ;
}
}
}
foreach ( var item in settingsBackupFiles )
{
var backupTime = DateTime . FromFileTimeUtc ( item . Key ) ;
if ( backupTime < deleteIfOlder )
{
try
{
Logger . LogInfo ( $"RemoveOldBackups killing {item.Value}" ) ;
File . Delete ( item . Value ) ;
}
catch ( Exception ex2 )
{
Logger . LogError ( $"Failed to remove a setting backup folder ({item.Value}), because: ({ex2.Message})" ) ;
}
}
}
}
finally
{
2024-11-13 12:36:45 -05:00
removeOldBackupsLock . Exit ( ) ;
2022-10-13 03:41:21 -04:00
}
}
/// <summary>
/// Class <c>JsonNormalizer</c> is a utility class to 'normalize' a JSON file so that it can be compared to another JSON file.
/// This really just means to fully sort it. This does not work for any JSON file where the order of the node is relevant.
/// </summary>
2023-03-16 15:51:31 +01:00
private sealed class JsonNormalizer
2022-10-13 03:41:21 -04:00
{
public static string Normalize ( string json )
{
var doc1 = JsonNormalizer . Deserialize ( json ) ;
2023-12-28 13:37:13 +03:00
var newJson1 = JsonSerializer . Serialize ( doc1 , _serializerOptions ) ;
2022-10-13 03:41:21 -04:00
return newJson1 ;
}
private static List < object > DeserializeArray ( string json )
{
var result = JsonSerializer . Deserialize < List < object > > ( json ) ;
var updates = new List < object > ( ) ;
foreach ( var item in result )
{
if ( item ! = null )
{
var currentItem = ( JsonElement ) item ;
if ( currentItem . ValueKind = = JsonValueKind . Object )
{
updates . Add ( Deserialize ( currentItem . ToString ( ) ) ) ;
}
else if ( ( ( JsonElement ) item ) . ValueKind = = JsonValueKind . Array )
{
updates . Add ( DeserializeArray ( currentItem . ToString ( ) ) ) ;
}
else
{
updates . Add ( item ) ;
}
}
else
{
updates . Add ( item ) ;
}
}
return updates . OrderBy ( x = > JsonSerializer . Serialize ( x ) ) . ToList ( ) ;
}
private static Dictionary < string , object > Deserialize ( string json )
{
var doc = JsonSerializer . Deserialize < Dictionary < string , object > > ( json ) ;
var updates = new Dictionary < string , object > ( ) ;
foreach ( var item in doc )
{
if ( item . Value ! = null )
{
if ( ( ( JsonElement ) item . Value ) . ValueKind = = JsonValueKind . Object )
{
updates . Add ( item . Key , Deserialize ( ( ( JsonElement ) item . Value ) . ToString ( ) ) ) ;
}
else if ( ( ( JsonElement ) item . Value ) . ValueKind = = JsonValueKind . Array )
{
updates . Add ( item . Key , DeserializeArray ( ( ( JsonElement ) item . Value ) . ToString ( ) ) ) ;
}
}
}
foreach ( var item in updates )
{
doc . Remove ( item . Key ) ;
doc . Add ( item . Key , item . Value ) ;
}
var ordered = new Dictionary < string , object > ( ) ;
foreach ( var item in doc . Keys . OrderBy ( x = > x ) )
{
ordered . Add ( item , doc [ item ] ) ;
}
return ordered ;
}
}
}
}