// Cleanup orphaned/dangling entries in FutureAccessList private void DeleteOrphanedTokensInFutureAccessList(NotepadsSessionDataV1 sessionData) { HashSet <string> tokens = sessionData.TextEditors .SelectMany(editor => new[] { editor.EditingFileFutureAccessToken }) .Where(token => token != null) .ToHashSet(StringComparer.OrdinalIgnoreCase); var tokensToBeDeleted = new List <string>(); foreach (var entry in StorageApplicationPermissions.FutureAccessList.Entries) { if (!tokens.Contains(entry.Token)) { tokensToBeDeleted.Add(entry.Token); } } foreach (var tokenToBeDel in tokensToBeDeleted) { try { StorageApplicationPermissions.FutureAccessList.Remove(tokenToBeDel); } catch (Exception ex) { LoggingService.LogError($"[SessionManager] Failed to delete orphaned token in FutureAccessList: {ex.Message}"); } } }
private async Task DeleteOrphanedBackupFilesAsync(NotepadsSessionDataV1 sessionData) { HashSet <string> backupPaths = sessionData.TextEditors .SelectMany(te => new[] { te.LastSaved?.BackupFilePath, te.Pending?.BackupFilePath }) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (StorageFile backupFile in await SessionUtility.GetAllBackupFilesAsync()) { if (!backupPaths.Contains(backupFile.Path)) { try { await backupFile.DeleteAsync(); } catch (Exception ex) { LoggingService.LogError($"Failed to delete backup file: {ex.Message}"); } } } }
private async Task DeleteOrphanedBackupFilesAsync(NotepadsSessionDataV1 sessionData) { HashSet <string> backupPaths = sessionData.TextEditors .SelectMany(te => new[] { te.LastSaved?.BackupFilePath, te.Pending?.BackupFilePath }) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (StorageFile backupFile in await SessionUtility.GetAllBackupFilesAsync()) { if (!backupPaths.Contains(backupFile.Path)) { try { await backupFile.DeleteAsync(); } catch { // Best effort } } } }
// Cleanup orphaned/dangling backup files private async Task DeleteOrphanedBackupFilesAsync(NotepadsSessionDataV1 sessionData) { HashSet <string> backupPaths = sessionData.TextEditors .SelectMany(editor => new[] { editor.LastSavedBackupFilePath, editor.PendingBackupFilePath }) .Where(path => path != null) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (StorageFile backupFile in await SessionUtility.GetAllBackupFilesAsync(_backupFolderName)) { if (!backupPaths.Contains(backupFile.Path)) { try { await backupFile.DeleteAsync(); } catch (Exception ex) { LoggingService.LogError($"[{nameof(SessionManager)}] Failed to delete orphaned backup file: {ex.Message}"); } } } }
public async Task SaveSessionAsync(Action actionAfterSaving = null) { if (!IsBackupEnabled) { LoggingService.LogInfo("[SessionManager] Session backup is disabled."); return; } // Serialize saves await _semaphoreSlim.WaitAsync(); if (!IsBackupEnabled) { return; // Check again after SemaphoreSlim released } Stopwatch stopwatch = Stopwatch.StartNew(); ITextEditor[] textEditors = _notepadsCore.GetAllTextEditors(); if (textEditors == null || textEditors.Length == 0) { await ClearSessionDataAsync(); actionAfterSaving?.Invoke(); _semaphoreSlim.Release(); return; } ITextEditor selectedTextEditor = _notepadsCore.GetSelectedTextEditor(); NotepadsSessionDataV1 sessionData = new NotepadsSessionDataV1(); foreach (ITextEditor textEditor in textEditors) { if (_sessionData.TryGetValue(textEditor.Id, out TextEditorSessionDataV1 textEditorData)) { // Get latest state meta data textEditorData.StateMetaData = textEditor.GetTextEditorStateMetaData(); } else // Text content has been changed or editor has not backed up yet { textEditorData = new TextEditorSessionDataV1 { Id = textEditor.Id, }; if (textEditor.EditingFile != null) { // Add the opened file to FutureAccessList so we can access it next launch var futureAccessToken = ToToken(textEditor.Id); await FileSystemUtility.TryAddOrReplaceTokenInFutureAccessList(futureAccessToken, textEditor.EditingFile); textEditorData.EditingFileFutureAccessToken = futureAccessToken; textEditorData.EditingFileName = textEditor.EditingFileName; textEditorData.EditingFilePath = textEditor.EditingFilePath; } if (textEditor.IsModified) { if (textEditor.EditingFile != null) { // Persist the last save known to the app, which might not be up-to-date (if the file was modified outside the app) var lastSavedBackupFile = await SessionUtility.CreateNewFileInBackupFolderAsync(ToToken(textEditor.Id) + "-LastSaved", CreationCollisionOption.ReplaceExisting); if (!await BackupTextAsync(textEditor.LastSavedSnapshot.Content, textEditor.LastSavedSnapshot.Encoding, textEditor.LastSavedSnapshot.LineEnding, lastSavedBackupFile)) { continue; } textEditorData.LastSavedBackupFilePath = lastSavedBackupFile.Path; } if (textEditor.EditingFile == null || !string.Equals(textEditor.LastSavedSnapshot.Content, textEditor.GetText())) { // Persist pending changes relative to the last save var pendingBackupFile = await SessionUtility.CreateNewFileInBackupFolderAsync(ToToken(textEditor.Id) + "-Pending", CreationCollisionOption.ReplaceExisting); if (!await BackupTextAsync(textEditor.GetText(), textEditor.LastSavedSnapshot.Encoding, textEditor.LastSavedSnapshot.LineEnding, pendingBackupFile)) { continue; } textEditorData.PendingBackupFilePath = pendingBackupFile.Path; } } textEditorData.StateMetaData = textEditor.GetTextEditorStateMetaData(); // We will not create new backup files for this text editor unless it has changes _sessionData.TryAdd(textEditor.Id, textEditorData); } sessionData.TextEditors.Add(textEditorData); if (textEditor == selectedTextEditor) { sessionData.SelectedTextEditor = textEditor.Id; } } sessionData.TabScrollViewerHorizontalOffset = _notepadsCore.GetTabScrollViewerHorizontalOffset(); bool sessionDataSaved = false; try { string sessionJsonStr = JsonConvert.SerializeObject(sessionData, Formatting.Indented); if (_lastSessionJsonStr == null || !string.Equals(_lastSessionJsonStr, sessionJsonStr, StringComparison.OrdinalIgnoreCase)) { // write await SessionUtility.SaveSerializedSessionMetaDataAsync(sessionJsonStr); _lastSessionJsonStr = sessionJsonStr; sessionDataSaved = true; } } catch (Exception ex) { LoggingService.LogError($"[SessionManager] Failed to save session metadata: {ex.Message}"); actionAfterSaving?.Invoke(); _semaphoreSlim.Release(); return; // Failed to save the session - do not proceed to delete backup files } if (sessionDataSaved) { await DeleteOrphanedBackupFilesAsync(sessionData); DeleteOrphanedTokensInFutureAccessList(sessionData); } stopwatch.Stop(); if (sessionDataSaved) { LoggingService.LogInfo($"[SessionManager] Successfully saved the current session. Total time: {stopwatch.Elapsed.TotalMilliseconds} milliseconds.", consoleOnly: true); } actionAfterSaving?.Invoke(); _semaphoreSlim.Release(); }
public async Task SaveSessionAsync(Action actionAfterSaving = null) { if (!IsBackupEnabled) { LoggingService.LogInfo($"[{nameof(SessionManager)}] Session backup is disabled."); return; } // Serialize saves await _semaphoreSlim.WaitAsync(); if (!IsBackupEnabled) { return; // Check again after SemaphoreSlim released } Stopwatch stopwatch = Stopwatch.StartNew(); ITextEditor[] textEditors = _notepadsCore.GetAllTextEditors(); if (textEditors == null || textEditors.Length == 0) { await ClearSessionDataAsync(); actionAfterSaving?.Invoke(); _semaphoreSlim.Release(); return; } ITextEditor selectedTextEditor = _notepadsCore.GetSelectedTextEditor(); NotepadsSessionDataV1 sessionData = new NotepadsSessionDataV1(); foreach (ITextEditor textEditor in textEditors) { try { var textEditorSessionData = await GetTextEditorSessionData(textEditor); if (textEditorSessionData == null) { continue; } sessionData.TextEditors.Add(textEditorSessionData); if (textEditor == selectedTextEditor) { sessionData.SelectedTextEditor = textEditor.Id; } } catch (Exception ex) { LoggingService.LogError($"[{nameof(SessionManager)}] Failed to build TextEditor session data: {ex}"); Analytics.TrackEvent("SessionManager_FailedToBuildTextEditorSessionData", new Dictionary <string, string>() { { "Exception", ex.Message } }); } } sessionData.TabScrollViewerHorizontalOffset = _notepadsCore.GetTabScrollViewerHorizontalOffset(); bool sessionDataSaved = false; try { string sessionJsonStr = JsonConvert.SerializeObject(sessionData, Formatting.Indented); if (_lastSessionJsonStr == null || !string.Equals(_lastSessionJsonStr, sessionJsonStr, StringComparison.OrdinalIgnoreCase)) { // write await SessionUtility.SaveSerializedSessionMetaDataAsync(sessionJsonStr, _sessionMetaDataFileName); _lastSessionJsonStr = sessionJsonStr; sessionDataSaved = true; } } catch (Exception ex) { LoggingService.LogError($"[{nameof(SessionManager)}] Failed to save session metadata: {ex.Message}"); Analytics.TrackEvent("SessionManager_FailedToSaveSessionMetaData", new Dictionary <string, string>() { { "Exception", ex.Message } }); actionAfterSaving?.Invoke(); _semaphoreSlim.Release(); return; // Failed to save the session - do not proceed to delete backup files } if (sessionDataSaved) { try { await DeleteOrphanedBackupFilesAsync(sessionData); DeleteOrphanedTokensInFutureAccessList(sessionData); } catch (Exception ex) { Analytics.TrackEvent("SessionManager_FailedToDeleteOrphanedBackupFiles", new Dictionary <string, string>() { { "Exception", ex.Message } }); } } stopwatch.Stop(); if (sessionDataSaved) { LoggingService.LogInfo($"[{nameof(SessionManager)}] Successfully saved the current session. Total time: {stopwatch.Elapsed.TotalMilliseconds} milliseconds.", consoleOnly: true); } actionAfterSaving?.Invoke(); _semaphoreSlim.Release(); }
public async Task SaveSessionAsync() { if (!IsBackupEnabled) { LoggingService.LogInfo("Session backup is disabled."); return; } // Serialize saves await _semaphoreSlim.WaitAsync(); Stopwatch stopwatch = Stopwatch.StartNew(); ITextEditor[] textEditors = _notepadsCore.GetAllTextEditors(); ITextEditor selectedTextEditor = _notepadsCore.GetSelectedTextEditor(); FileSystemUtility.ClearFutureAccessList(); NotepadsSessionDataV1 sessionData = new NotepadsSessionDataV1(); foreach (ITextEditor textEditor in textEditors) { if (textEditor.EditingFile != null) { // Add the opened file to FutureAccessList so we can access it next launch await FileSystemUtility.TryAddToFutureAccessList(ToToken(textEditor.Id), textEditor.EditingFile); } if (!_sessionData.TryGetValue(textEditor.Id, out TextEditorSessionData textEditorData)) { textEditorData = new TextEditorSessionData { Id = textEditor.Id }; if (textEditor.IsModified) { if (textEditor.EditingFile != null) { // Persist the last save known to the app, which might not be up-to-date (if the file was modified outside the app) BackupMetadata lastSaved = await SaveLastSavedChangesAsync(textEditor); if (lastSaved == null) { continue; } textEditorData.LastSaved = lastSaved; } // Persist pending changes relative to the last save BackupMetadata pending = await SavePendingChangesAsync(textEditor); if (pending == null) { continue; } textEditorData.Pending = pending; } // We will not create new backup files for this text editor unless it has changes _sessionData.TryAdd(textEditor.Id, textEditorData); } sessionData.TextEditors.Add(textEditorData); if (textEditor == selectedTextEditor) { sessionData.SelectedTextEditor = textEditor.Id; } } bool sessionDataSaved = false; try { string sessionJsonStr = JsonConvert.SerializeObject(sessionData, _encodingConverter); if (!(ApplicationSettingsStore.Read(SessionDataKey) is string currentValue) || !string.Equals(currentValue, sessionJsonStr, StringComparison.OrdinalIgnoreCase)) { ApplicationSettingsStore.Write(SessionDataKey, sessionJsonStr); sessionDataSaved = true; } } catch (Exception ex) { LoggingService.LogError($"Failed to save session metadata: {ex.Message}"); return; // Failed to save the session - do not proceed to delete backup files } if (sessionDataSaved) { await DeleteOrphanedBackupFilesAsync(sessionData); } stopwatch.Stop(); if (sessionDataSaved) { LoggingService.LogInfo($"Successfully saved the current session. Total time: {stopwatch.Elapsed.TotalMilliseconds} milliseconds.", consoleOnly: true); } _semaphoreSlim.Release(); }
public async Task SaveSessionAsync() { if (!IsBackupEnabled) { return; } // Serialize saves await _semaphoreSlim.WaitAsync(); NotepadsSessionDataV1 sessionData = new NotepadsSessionDataV1(); HashSet <string> backupPaths = new HashSet <string>(StringComparer.OrdinalIgnoreCase); TextEditor[] textEditors = _notepadsCore.GetAllTextEditors(); TextEditor selectedTextEditor = _notepadsCore.GetSelectedTextEditor(); FileSystemUtility.ClearFutureAccessList(); foreach (TextEditor textEditor in textEditors) { TextEditorSessionData textEditorData = new TextEditorSessionData { Id = textEditor.Id }; if (textEditor.EditingFile != null) { // Add the opened file to FutureAccessList so we can access it next launch FileSystemUtility.TryAddToFutureAccessList(ToToken(textEditor.Id), textEditor.EditingFile); // Persist the last save known to the app, which might not be up-to-date (if the file was modified outside the app) BackupMetadata lastSaved = await SaveLastSavedChangesAsync(textEditor); if (lastSaved == null) { continue; } textEditorData.LastSaved = lastSaved; backupPaths.Add(lastSaved.BackupFilePath); } if (textEditor.IsModified) { // Persist pending changes relative to the last save BackupMetadata pending = await SavePendingChangesAsync(textEditor); if (pending == null) { continue; } textEditorData.Pending = pending; backupPaths.Add(pending.BackupFilePath); } if (textEditorData.LastSaved != null || textEditorData.Pending != null) { sessionData.TextEditors.Add(textEditorData); if (textEditor == selectedTextEditor) { sessionData.SelectedTextEditor = textEditor.Id; } } } try { string sessionJson = JsonConvert.SerializeObject(sessionData, _encodingConverter); ApplicationData.Current.LocalSettings.Values[SessionDataKey] = sessionJson; LoggingService.LogInfo("Successfully saved the current session."); } catch { return; // Failed to save the session - do not proceed to delete backup files } await DeleteOrphanedBackupFilesAsync(backupPaths); _semaphoreSlim.Release(); }
public async Task SaveSessionAsync() { if (!IsBackupEnabled) { LoggingService.LogInfo("Session backup is disabled."); return; } // Serialize saves await _semaphoreSlim.WaitAsync(); StorageFolder backupFolder = await SessionUtility.GetBackupFolderAsync(); LoggingService.LogInfo("Session backup is starting. Backup folder: " + backupFolder.Path); Stopwatch stopwatch = Stopwatch.StartNew(); TextEditor[] textEditors = _notepadsCore.GetAllTextEditors(); TextEditor selectedTextEditor = _notepadsCore.GetSelectedTextEditor(); FileSystemUtility.ClearFutureAccessList(); NotepadsSessionDataV1 sessionData = new NotepadsSessionDataV1(); foreach (TextEditor textEditor in textEditors) { if (textEditor.EditingFile != null) { // Add the opened file to FutureAccessList so we can access it next launch FileSystemUtility.TryAddToFutureAccessList(ToToken(textEditor.Id), textEditor.EditingFile); } if (!_sessionData.TryGetValue(textEditor.Id, out TextEditorSessionData textEditorData)) { textEditorData = new TextEditorSessionData { Id = textEditor.Id }; if (textEditor.EditingFile != null) { // Persist the last save known to the app, which might not be up-to-date (if the file was modified outside the app) BackupMetadata lastSaved = await SaveLastSavedChangesAsync(textEditor); if (lastSaved == null) { continue; } textEditorData.LastSaved = lastSaved; } if (textEditor.IsModified) { // Persist pending changes relative to the last save BackupMetadata pending = await SavePendingChangesAsync(textEditor); if (pending == null) { continue; } textEditorData.Pending = pending; } // We will not create new backup files for this text editor unless it has changes _sessionData.TryAdd(textEditor.Id, textEditorData); } if (textEditorData.LastSaved != null || textEditorData.Pending != null) { sessionData.TextEditors.Add(textEditorData); if (textEditor == selectedTextEditor) { sessionData.SelectedTextEditor = textEditor.Id; } } } try { string sessionJson = JsonConvert.SerializeObject(sessionData, _encodingConverter); ApplicationData.Current.LocalSettings.Values[SessionDataKey] = sessionJson; } catch { return; // Failed to save the session - do not proceed to delete backup files } await DeleteOrphanedBackupFilesAsync(sessionData); stopwatch.Stop(); LoggingService.LogInfo("Successfully saved the current session. Total time: " + stopwatch.Elapsed.TotalMilliseconds + " milliseconds."); _semaphoreSlim.Release(); }