Skip to content

Commit e0e2341

Browse files
authored
Merge pull request #10 from emu-sync/dev
Local backups + restore + syc logs
2 parents 0f8e0ad + 94d05a4 commit e0e2341

217 files changed

Lines changed: 2747 additions & 717 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,20 @@ This is the same release as 1.0.2, but with a fix for Windows not correctly iden
5656
# v1.0.5
5757

5858
## Fixes
59-
- Fixed an issue where nested folder structure wasn't being retained on restore from Windows to Linux.
59+
- Fixed an issue where nested folder structure wasn't being retained on restore from Windows to Linux.
60+
61+
# v1.0.6
62+
63+
## Local game backups
64+
- EmuSync automatically creates a local backup of your game save before downloading a newer version.
65+
- Backups are stored locally on each of your devices.
66+
- Control how many backups are kept in the **This device** section (default: 10).
67+
- Easily restore from any previous backup if an incorrect save overwrites your progress.
68+
- Ideal for recovering from incorrect sync issues.
69+
70+
## Local sync history
71+
- You can now keep track of when EmuSync uploads or downloads files for your configured games.
72+
- Useful for troubleshooting and understanding exactly when AutoSync occurs.
73+
74+
## Other
75+
- Tweaked the layout of several sections to provide clearer visual structure (hopefully!).

NEWS.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ There is a now Decky plugin available for SteamDeck users! Please check out how
33

44
The status of your game syncs can now be checked via the Decky plugin, as well as manually syncing your game saves - no more hopping into desktop mode each time.
55

6-
## Features I plan to add
6+
## Local game backups & restore mechanism
7+
The latest version of EmuSync will now keep local backups whenever it downloads new files, so if something goes horribly wrong, there is a now a disaster recovery option!
78

8-
- Device specific sync log/history so you can review when saves were uploaded from/downloaded to the device.
9-
- Local backups of game save files whenever new files are downloaded to a device.
10-
- This will also include a restore from backup mechanism.
9+
Read more about game backups [here](https://github.com/emu-sync/EmuSync/wiki/Local-game-backups).

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
## 📖 About
88

99
Game save files are stored in different places across OS platforms, which can make syncing a challenge.
10-
EmuSync aims to simplify this by mapping locations from multiple devices to a single sync point.
10+
EmuSync aims to simplify this by mapping locations from multiple devices to a single sync point, using your chosen cloud storage provider as the storage point.
1111

12-
Easily set up which games you want keep backed up and let EmuSync handle the rest! It even works in Gaming mode on the SteamDeck - check out the Decky plugin [here](https://github.com/emu-sync/EmuSync-DeckyPlugin).
12+
Easily set up which games you want to keep backed up and let EmuSync handle the rest! It even works in Gaming mode on the SteamDeck - check out the Decky plugin [here](https://github.com/emu-sync/EmuSync-DeckyPlugin).
1313

1414
- If you're having trouble with EmuSync, or want to see in more detail how to use it, please see our [Wiki](https://github.com/emu-sync/EmuSync/wiki).
1515
- Want to see what EmuSync looks like before you download it? Check out the [Preview](https://github.com/emu-sync/EmuSync/wiki/Preview-EmuSync) page in our wiki.
@@ -38,7 +38,8 @@ Confirmed working devices (but not limited to):
3838
- Define and manage your game save locations.
3939
- Automatically detect game saves, making the configuration of your game syncs a breeze.
4040
- Automatically and manually sync game saves.
41-
- A Decky plugin for SteamDeck users so you can check on the status of your game syncs without having to hop into desktop mode
41+
- Local backup & restore mechanism.
42+
- A Decky plugin for SteamDeck users so you can check on the status of your game syncs without having to hop into desktop mode.
4243
- [EmuSync-DeckyPlugin](https://github.com/emu-sync/EmuSync-DeckyPlugin)
4344

4445
## 🛠️ Installation

src/EmuSync.Agent/Background/GameSyncWorker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private async Task<TimeSpan> TryGetLoopDelayAsync(CancellationToken cancellation
7171

7272
return syncSource?.AutoSyncFrequency ?? _options.LoopDelayTimeSpan;
7373
}
74-
catch (Exception ex)
74+
catch
7575
{
7676
return _options.LoopDelayTimeSpan;
7777
}

src/EmuSync.Agent/Background/LudusaviManifestWorker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
5252
{
5353
var result = await _manifestScanner.ScanForSaveFilesAsync(gameDefinitions, cancellationToken);
5454

55-
_lastScanTime = DateTime.UtcNow;
5655
_countOfGamesFound = result.FoundGames.Count;
5756
}
5857
}
@@ -62,6 +61,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
6261
}
6362
finally
6463
{
64+
_lastScanTime = DateTime.UtcNow;
6565
_scanInProgress = false;
6666
}
6767
}

src/EmuSync.Agent/Controllers/GameController.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using EmuSync.Agent.Dto.Game;
22
using EmuSync.Domain;
33
using EmuSync.Domain.Extensions;
4-
using EmuSync.Domain.Services;
54
using EmuSync.Domain.Services.Interfaces;
65
using EmuSync.Services.LudusaviImporter;
76
using EmuSync.Services.Managers.Interfaces;
@@ -18,7 +17,8 @@ public class GameController(
1817
IGameSyncStatusCache gameSyncStatusCache,
1918
IGameSyncService gameSyncService,
2019
ISyncTasks syncTasks,
21-
IApiCache apiCache
20+
IApiCache apiCache,
21+
ILocalGameSaveBackupService localGameSaveBackupService
2222
) : CustomControllerBase(logger, validator)
2323
{
2424
private readonly IGameManager _manager = manager;
@@ -27,9 +27,10 @@ IApiCache apiCache
2727
private readonly IGameSyncService _gameSyncService = gameSyncService;
2828
private readonly ISyncTasks _syncTasks = syncTasks;
2929
private readonly IApiCache _apiCache = apiCache;
30+
private readonly ILocalGameSaveBackupService _localGameSaveBackupService = localGameSaveBackupService;
3031

3132
[HttpGet]
32-
public async Task<IActionResult> GetList( CancellationToken cancellationToken = default)
33+
public async Task<IActionResult> GetList(CancellationToken cancellationToken = default)
3334
{
3435
List<GameEntity>? list = _apiCache.Games.Value;
3536

@@ -39,7 +40,7 @@ public async Task<IActionResult> GetList( CancellationToken cancellationToken =
3940
if (list != null) _apiCache.Games.Set(list);
4041
}
4142

42-
//if we have games, just reprocess all the file watchers that might be attached / re-evaluate the sync statuses
43+
//if we have games, just re-evaluate the sync statuses
4344
if (list != null && list.Count > 0)
4445
{
4546
await _gameSyncService.TryDetectGameSyncStatusesAsync(list, cancellationToken);
@@ -61,7 +62,7 @@ public async Task<IActionResult> GetList( CancellationToken cancellationToken =
6162
}
6263

6364
[HttpGet("{id}")]
64-
public async Task<IActionResult> GetById(string id, CancellationToken cancellationToken = default)
65+
public async Task<IActionResult> GetById([FromRoute]string id, CancellationToken cancellationToken = default)
6566
{
6667
GameEntity? entity = _apiCache.GetGame(id);
6768
entity ??= await _manager.GetAsync(id, cancellationToken);
@@ -71,7 +72,6 @@ public async Task<IActionResult> GetById(string id, CancellationToken cancellati
7172
return NotFoundWithErrors($"No game found with ID {id}");
7273
}
7374

74-
//even if we fetch, just update the watcher - we might have an outdated on this device
7575
await TryUpdateSyncTaskAsync(entity, cancellationToken);
7676

7777
GameDto response = entity.ToDto();
@@ -89,6 +89,16 @@ public async Task<IActionResult> GetSuggestions(CancellationToken cancellationTo
8989
return Ok(response);
9090
}
9191

92+
[HttpGet("{id}/Backups")]
93+
public async Task<IActionResult> GetGameBackups([FromRoute] string id, CancellationToken cancellationToken = default)
94+
{
95+
List<LocalGameBackupManifestEntity> manifests = await _localGameSaveBackupService.GetBackupsAsync(id, cancellationToken);
96+
97+
List<GameBackupManifestDto> response = manifests.ConvertAll(x => x.ToDto());
98+
99+
return Ok(response);
100+
}
101+
92102
[HttpPost]
93103
public async Task<IActionResult> Create([FromBody] CreateGameDto requestBody, CancellationToken cancellationToken = default)
94104
{

src/EmuSync.Agent/Controllers/GameSyncController.cs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public async Task<IActionResult> SyncNow([FromRoute] string id, CancellationToke
7979
return NotFoundWithErrors($"No game found with ID {id}");
8080
}
8181

82-
await _manager.SyncGameAsync(syncSource.Id, game, cancellationToken);
82+
await _manager.SyncGameAsync(syncSource.Id, game, isAutoSync: false, cancellationToken);
8383

8484
_gameSyncStatusCache.AddOrUpdate(id, GameSyncStatus.InSync);
8585

@@ -89,7 +89,7 @@ public async Task<IActionResult> SyncNow([FromRoute] string id, CancellationToke
8989
[HttpPost("{id}/ForceUpload")]
9090
public async Task<IActionResult> ForceUpload([FromRoute] string id, CancellationToken cancellationToken = default)
9191
{
92-
LogRequest($"{nameof(ForceUpload)}/{id}");
92+
LogRequest($"{id}/{nameof(ForceUpload)}");
9393

9494
SyncSourceEntity? syncSource = await _syncSourceManager.GetLocalAsync(cancellationToken);
9595

@@ -106,7 +106,7 @@ public async Task<IActionResult> ForceUpload([FromRoute] string id, Cancellation
106106
return NotFoundWithErrors($"No game found with ID {id}");
107107
}
108108

109-
await _manager.ForceUploadGameAsync(syncSource.Id, game, cancellationToken);
109+
await _manager.ForceUploadGameAsync(syncSource.Id, game, isAutoSync: false, cancellationToken);
110110

111111
_gameSyncStatusCache.AddOrUpdate(id, GameSyncStatus.InSync);
112112

@@ -116,7 +116,7 @@ public async Task<IActionResult> ForceUpload([FromRoute] string id, Cancellation
116116
[HttpPost("{id}/ForceDownload")]
117117
public async Task<IActionResult> ForceDownload([FromRoute] string id, CancellationToken cancellationToken = default)
118118
{
119-
LogRequest($"{nameof(ForceDownload)}/{id}");
119+
LogRequest($"{id}/{nameof(ForceDownload)}");
120120

121121
SyncSourceEntity? syncSource = await _syncSourceManager.GetLocalAsync(cancellationToken);
122122

@@ -133,7 +133,34 @@ public async Task<IActionResult> ForceDownload([FromRoute] string id, Cancellati
133133
return NotFoundWithErrors($"No game found with ID {id}");
134134
}
135135

136-
await _manager.ForceDownloadGameAsync(syncSource.Id, game, cancellationToken);
136+
await _manager.ForceDownloadGameAsync(syncSource.Id, game, isAutoSync: false, cancellationToken);
137+
138+
_gameSyncStatusCache.AddOrUpdate(id, GameSyncStatus.InSync);
139+
140+
return Ok();
141+
}
142+
143+
[HttpPost("{id}/RestoreFromBackup/{backupId}")]
144+
public async Task<IActionResult> RestoreFromBackup([FromRoute] string id, [FromRoute] string backupId, CancellationToken cancellationToken = default)
145+
{
146+
LogRequest($"{id}/{nameof(RestoreFromBackup)}/{backupId}");
147+
148+
SyncSourceEntity? syncSource = await _syncSourceManager.GetLocalAsync(cancellationToken);
149+
150+
if (syncSource == null)
151+
{
152+
return BadRequest("No sync source has been set up");
153+
}
154+
155+
//always fetch latest game on force
156+
GameEntity? game = await _gameManager.GetAsync(id, cancellationToken);
157+
158+
if (game == null)
159+
{
160+
return NotFoundWithErrors($"No game found with ID {id}");
161+
}
162+
163+
await _manager.RestoreFromBackup(syncSource.Id, game, backupId, cancellationToken);
137164

138165
_gameSyncStatusCache.AddOrUpdate(id, GameSyncStatus.InSync);
139166

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using EmuSync.Agent.Dto.LocalSyncLog;
2+
using EmuSync.Domain.Services.Interfaces;
3+
4+
namespace EmuSync.Agent.Controllers;
5+
6+
[ApiController]
7+
[Route("[controller]")]
8+
public class LocalSyncLogController(
9+
ILogger<LocalSyncLogController> logger,
10+
IValidationService validator,
11+
ILocalSyncLog localSyncLog
12+
) : CustomControllerBase(logger, validator)
13+
{
14+
private readonly ILocalSyncLog _localSyncLog = localSyncLog;
15+
16+
[HttpGet]
17+
public async Task<IActionResult> GetAllLogs(CancellationToken cancellationToken = default)
18+
{
19+
List<LocalSyncLogEntity> localSyncLogs = await _localSyncLog.GetAllLogsAsync(cancellationToken);
20+
21+
List<LocalSyncLogDto> response = localSyncLogs.ConvertAll(log => log.ToDto());
22+
23+
return Ok(response);
24+
}
25+
26+
[HttpGet("Game/{gameId}")]
27+
public async Task<IActionResult> GetLogsForGame(string gameId, CancellationToken cancellationToken = default)
28+
{
29+
List<LocalSyncLogEntity> localSyncLogs = await _localSyncLog.GetAllLogsForGameAsync(gameId, cancellationToken);
30+
31+
List<LocalSyncLogDto> response = localSyncLogs.ConvertAll(log => log.ToDto());
32+
33+
return Ok(response);
34+
}
35+
}

src/EmuSync.Agent/Controllers/SyncSourceController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public async Task<IActionResult> GetGameScanDetails(CancellationToken cancellati
6363

6464
var diff = lastScanTime - now;
6565

66-
GameScanDetails response = new()
66+
GameScanDetailsDto response = new()
6767
{
6868
LastScanSeconds = diff.TotalSeconds,
6969
InProgress = LudusaviManifestWorker.ScanInProgress,

src/EmuSync.Agent/Controllers/SystemController.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using EmuSync.Agent.Dto.System;
2-
using EmuSync.Services.LudusaviImporter;
32
using System.Reflection;
43

54
namespace EmuSync.Agent.Controllers;

0 commit comments

Comments
 (0)