Skip to content

Commit aea6b4a

Browse files
authored
Merge pull request #13 from emu-sync/dev
Rework upload/download to cater for larger folders
2 parents e0e2341 + 0a1d932 commit aea6b4a

31 files changed

Lines changed: 778 additions & 159 deletions

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,11 @@ This is the same release as 1.0.2, but with a fix for Windows not correctly iden
7272
- Useful for troubleshooting and understanding exactly when AutoSync occurs.
7373

7474
## Other
75-
- Tweaked the layout of several sections to provide clearer visual structure (hopefully!).
75+
- Tweaked the layout of several sections to provide clearer visual structure (hopefully!).
76+
77+
# v1.0.7
78+
79+
- Added a progress indicator when syncing game files
80+
- Reworked how EmuSync uploads/downloads files
81+
- This is mostly for people who are uploading larger folders and may have experienced issues where they'd fail
82+
- Various minor bugixes related to the Dropbox storage provider

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ The status of your game syncs can now be checked via the Decky plugin, as well a
77
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!
88

99
Read more about game backups [here](https://github.com/emu-sync/EmuSync/wiki/Local-game-backups).
10+
11+
## Features I plan to add
12+
- I'm planning to add options to override backup settings per game, such as changing the amount of backups to keep

src/EmuSync.Agent/Background/SyncTaskWorker.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1-
namespace EmuSync.Agent.Background;
1+
using EmuSync.Domain;
2+
using EmuSync.Domain.Services.Interfaces;
3+
4+
namespace EmuSync.Agent.Background;
25

36
public class SyncTaskWorker(
47
ILogger<SyncTaskWorker> logger,
58
IServiceProvider serviceProvider,
6-
ISyncTasks syncTasks
9+
ISyncTasks syncTasks,
10+
ILocalDataAccessor localDataAccessor
711
) : BackgroundService
812
{
913
private readonly ILogger<SyncTaskWorker> _logger = logger;
1014
private readonly IServiceProvider _serviceProvider = serviceProvider;
1115
private readonly ISyncTasks _syncTasks = syncTasks;
16+
private readonly ILocalDataAccessor _localDataAccessor = localDataAccessor;
1217

1318
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
1419
{
20+
//keep temporary zip folder tidy
21+
DeleteTemporaryZipFolder();
22+
1523
while (!cancellationToken.IsCancellationRequested)
1624
{
1725

@@ -37,6 +45,24 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
3745

3846
}
3947

48+
private void DeleteTemporaryZipFolder()
49+
{
50+
try
51+
{
52+
string path = _localDataAccessor.GetLocalFilePath(DomainConstants.LocalDataGameTempZipsFolder);
53+
54+
if (Directory.Exists(path))
55+
{
56+
Directory.Delete(path, true);
57+
}
58+
59+
}
60+
catch (Exception ex)
61+
{
62+
_logger.LogError(ex, "Failed to delete temp zip folder on startup");
63+
}
64+
}
65+
4066
private async Task TryProcessTasksAsync(CancellationToken cancellationToken)
4167
{
4268
try

src/EmuSync.Agent/Controllers/GameSyncController.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using EmuSync.Agent.Dto.GameSync;
22
using EmuSync.Domain.Enums;
3+
using EmuSync.Domain.Objects;
4+
using EmuSync.Domain.Services.Interfaces;
35
using EmuSync.Services.Managers.Interfaces;
46
using EmuSync.Services.Managers.Results;
7+
using System.Diagnostics.CodeAnalysis;
58

69
namespace EmuSync.Agent.Controllers;
710

@@ -13,13 +16,15 @@ public class GameSyncController(
1316
IGameSyncManager manager,
1417
IGameManager gameManager,
1518
ISyncSourceManager syncSourceManager,
16-
IGameSyncStatusCache gameSyncStatusCache
19+
IGameSyncStatusCache gameSyncStatusCache,
20+
ISyncProgressTracker syncProgressTracker
1721
) : CustomControllerBase(logger, validator)
1822
{
1923
private readonly IGameSyncManager _manager = manager;
2024
private readonly IGameManager _gameManager = gameManager;
2125
private readonly ISyncSourceManager _syncSourceManager = syncSourceManager;
2226
private readonly IGameSyncStatusCache _gameSyncStatusCache = gameSyncStatusCache;
27+
private readonly ISyncProgressTracker _syncProgressTracker = syncProgressTracker;
2328

2429
[HttpGet("{id}")]
2530
public async Task<IActionResult> GetSyncStatus([FromRoute] string id, CancellationToken cancellationToken = default)
@@ -166,4 +171,21 @@ public async Task<IActionResult> RestoreFromBackup([FromRoute] string id, [FromR
166171

167172
return Ok();
168173
}
174+
175+
[HttpGet("{id}/SyncProgress")]
176+
public async Task<IActionResult> SyncProgress([FromRoute] string id, CancellationToken cancellationToken = default)
177+
{
178+
SyncProgress? syncProgress = _syncProgressTracker.Get(id);
179+
180+
SyncProgressDto response = new()
181+
{
182+
InProgress = syncProgress != null,
183+
OverallCompletionPercent = syncProgress?.OverallCompletionPercent is double val
184+
? Math.Round(val, 2)
185+
: null,
186+
CurrentStage = syncProgress?.CurrentStage,
187+
};
188+
189+
return Ok(response);
190+
}
169191
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace EmuSync.Agent.Dto.GameSync;
4+
5+
public record SyncProgressDto
6+
{
7+
[JsonPropertyName("inProgress")]
8+
public bool InProgress { get; set; }
9+
10+
[JsonPropertyName("overallCompletionPercent")]
11+
public double? OverallCompletionPercent { get; set; }
12+
13+
[JsonPropertyName("currentStage")]
14+
public string? CurrentStage { get; set; }
15+
}

src/EmuSync.Agent/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using EmuSync.Domain.Enums;
77
using EmuSync.Domain.Extensions;
88
using EmuSync.Domain.Helpers;
9+
using EmuSync.Domain.Services;
10+
using EmuSync.Domain.Services.Interfaces;
911
using EmuSync.Services.LudusaviImporter;
1012
using EmuSync.Services.LudusaviImporter.Interfaces;
1113
using EmuSync.Services.Managers.Extensions;
@@ -134,6 +136,7 @@ private static void ConfigureServices(WebApplicationBuilder builder)
134136
#endregion
135137

136138
builder.Services.AddSingleton<ISyncTasks, SyncTasks>();
139+
builder.Services.AddSingleton<ISyncProgressTracker, SyncProgressTracker>();
137140

138141
builder.Services.AddSingleton<IApiCache, ApiCache>();
139142
builder.Services.AddSingleton<IGameSyncStatusCache, GameSyncStatusCache>();

src/EmuSync.Domain/DomainConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public class DomainConstants
66
public const string LocalDataFolder = ".emusync-data";
77

88
public const string LocalDataGameBackupFolder = "game-backups";
9+
public const string LocalDataGameTempZipsFolder = "temp-zips";
910
public const string LocalDataGameBackupFileNameFormat = "backup_{0}.zip";
1011
public const string LocalDataGameBackupManifestFile = "manifest.json";
1112

src/EmuSync.Domain/Helpers/ZipHelper.cs

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,60 @@ namespace EmuSync.Domain.Helpers;
55
public static class ZipHelper
66
{
77
/// <summary>
8-
/// Creates an in memory zip of all files and folders found at <paramref name="folderPath"/>
8+
/// Creates a zip of all files and folders found at <paramref name="folderPath"/>
9+
/// and writes it to <paramref name="zipPath"/>
910
/// </summary>
10-
/// <param name="folderPath"></param>
11-
/// <returns></returns>
12-
public static MemoryStream CreateZipFromFolder(string folderPath)
13-
11+
public static void CreateZipFromFolder(
12+
string folderPath,
13+
string zipPath,
14+
Action<double>? onProgressChange = null
15+
)
1416
{
15-
var memoryStream = new MemoryStream();
17+
var files = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories);
18+
int totalFiles = files.Length;
19+
int processedFiles = 0;
20+
21+
Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!);
22+
23+
using var fileStream = new FileStream(
24+
zipPath,
25+
FileMode.Create,
26+
FileAccess.Write,
27+
FileShare.None
28+
);
1629

17-
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
30+
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create);
31+
32+
foreach (var filePath in files)
1833
{
19-
foreach (var filePath in Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories))
20-
{
21-
var relativePath = Path.GetRelativePath(folderPath, filePath);
22-
var entry = archive.CreateEntry(relativePath, CompressionLevel.Optimal);
34+
var relativePath = Path.GetRelativePath(folderPath, filePath);
35+
var entry = archive.CreateEntry(relativePath, CompressionLevel.Optimal);
2336

24-
using var entryStream = entry.Open();
37+
using var entryStream = entry.Open();
38+
using var input = File.OpenRead(filePath);
39+
input.CopyTo(entryStream);
2540

26-
using var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
27-
fileStream.CopyTo(entryStream);
28-
}
41+
processedFiles++;
42+
onProgressChange?.Invoke(
43+
totalFiles == 0 ? 100 : (processedFiles / (double)totalFiles) * 100
44+
);
2945
}
30-
31-
memoryStream.Position = 0; // reset for reading
32-
return memoryStream;
3346
}
3447

48+
3549
/// <summary>
3650
/// Extracts the in-memory zip to <paramref name="outputDirectory"/>
3751
/// </summary>
3852
/// <param name="zipStream"></param>
3953
/// <param name="outputDirectory"></param>
4054
/// <param name="forceLastWriteTime"></param>
41-
public static void ExtractToDirectory(MemoryStream zipStream, string outputDirectory, DateTime? forceLastWriteTime = null)
55+
/// <param name="onProgressChange"></param>
56+
public static void ExtractToDirectory(
57+
Stream zipStream,
58+
string outputDirectory,
59+
DateTime? forceLastWriteTime = null,
60+
Action<double>? onProgressChange = null
61+
)
4262
{
4363
string? cleanOutputDirectory = GetOsSafePath(outputDirectory);
4464
if (string.IsNullOrEmpty(cleanOutputDirectory)) return;
@@ -53,7 +73,11 @@ public static void ExtractToDirectory(MemoryStream zipStream, string outputDirec
5373

5474
Directory.CreateDirectory(cleanOutputDirectory);
5575

56-
foreach (var entry in archive.Entries)
76+
var entries = archive.Entries.Where(e => !string.IsNullOrEmpty(e.Name)).ToList();
77+
int totalEntries = entries.Count;
78+
int processedEntries = 0;
79+
80+
foreach (var entry in entries)
5781
{
5882
var filePath = GetOsSafePath(
5983
Path.Combine(cleanOutputDirectory, entry.FullName)
@@ -85,6 +109,9 @@ public static void ExtractToDirectory(MemoryStream zipStream, string outputDirec
85109
{
86110
File.SetLastWriteTimeUtc(filePath, forceLastWriteTime.Value);
87111
}
112+
113+
processedEntries++;
114+
onProgressChange?.Invoke((processedEntries / (double)totalEntries) * 100);
88115
}
89116

90117
//stop false positives and ensure we keep the last write time on the local directory the same
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
namespace EmuSync.Domain.Objects;
2+
3+
public class ProgressStream : Stream
4+
{
5+
private readonly Stream _inner;
6+
private readonly Action<double>? _reportProgress;
7+
private readonly ulong _totalSize; // Explicit total size
8+
private long _totalRead;
9+
10+
public ProgressStream(Stream inner, Action<double>? reportProgress, ulong totalSize = 0)
11+
{
12+
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
13+
_reportProgress = reportProgress;
14+
15+
_totalSize = totalSize > 0 ? totalSize : (inner.CanSeek ? (ulong)inner.Length : 0);
16+
}
17+
18+
public override bool CanRead => _inner.CanRead;
19+
public override bool CanSeek => _inner.CanSeek;
20+
public override bool CanWrite => _inner.CanWrite;
21+
public override long Length => _inner.Length;
22+
public override long Position
23+
{
24+
get => _inner.Position;
25+
set => _inner.Position = value;
26+
}
27+
28+
public override void Flush() => _inner.Flush();
29+
30+
public override int Read(byte[] buffer, int offset, int count)
31+
{
32+
int read = _inner.Read(buffer, offset, count);
33+
Report(read);
34+
return read;
35+
}
36+
37+
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
38+
{
39+
int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken);
40+
Report(read);
41+
return read;
42+
}
43+
44+
public override void Write(byte[] buffer, int offset, int count)
45+
{
46+
_inner.Write(buffer, offset, count);
47+
Report(count);
48+
}
49+
50+
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
51+
{
52+
await _inner.WriteAsync(buffer, offset, count, cancellationToken);
53+
Report(count);
54+
}
55+
56+
private void Report(int count)
57+
{
58+
if (_reportProgress == null || count <= 0 || _totalSize <= 0) return;
59+
60+
_totalRead += count;
61+
double percent = (_totalRead / (double)_totalSize) * 100;
62+
_reportProgress(percent);
63+
}
64+
65+
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
66+
public override void SetLength(long value) => _inner.SetLength(value);
67+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace EmuSync.Domain.Objects;
2+
3+
public record SyncProgress
4+
{
5+
public double OverallCompletionPercent { get; set; }
6+
public string CurrentStage { get; set; }
7+
}

0 commit comments

Comments
 (0)