using System.IO; namespace EmbyToolbox.Services; public sealed class SafeFileReplaceService { public async Task ReplaceAsync( string originalPath, string finalPath, string tempOutputPath, IProgress? progress, CancellationToken cancellationToken) { if (!File.Exists(tempOutputPath)) { throw new IOException("Временный файл результата не найден."); } var tempInfo = new FileInfo(tempOutputPath); if (tempInfo.Length <= 0) { throw new IOException("Временный файл результата пуст."); } var backupPath = originalPath + ".bak_replace_tmp"; var finalBackupPath = finalPath + ".bak_existing_tmp"; if (File.Exists(backupPath)) { File.Delete(backupPath); } if (File.Exists(finalBackupPath)) { File.Delete(finalBackupPath); } File.Move(originalPath, backupPath, overwrite: false); try { if (!string.Equals(originalPath, finalPath, StringComparison.OrdinalIgnoreCase) && File.Exists(finalPath)) { File.Move(finalPath, finalBackupPath, overwrite: false); } var sameRoot = string.Equals(Path.GetPathRoot(finalPath), Path.GetPathRoot(tempOutputPath), StringComparison.OrdinalIgnoreCase); if (sameRoot) { File.Move(tempOutputPath, finalPath, overwrite: true); progress?.Report(99); } else { await CopyWithProgressAsync(tempOutputPath, finalPath, progress, cancellationToken).ConfigureAwait(false); File.Delete(tempOutputPath); } var outInfo = new FileInfo(finalPath); if (!outInfo.Exists || outInfo.Length <= 0) { throw new IOException("Новый файл после замены невалиден."); } File.Delete(backupPath); if (File.Exists(finalBackupPath)) { File.Delete(finalBackupPath); } } catch { try { if (File.Exists(finalPath)) { var i = new FileInfo(finalPath); if (i.Length == 0) { File.Delete(finalPath); } } } catch { // ignore } if (File.Exists(backupPath)) { File.Move(backupPath, originalPath, overwrite: true); } if (File.Exists(finalBackupPath)) { File.Move(finalBackupPath, finalPath, overwrite: true); } throw; } } private static async Task CopyWithProgressAsync(string src, string dst, IProgress? progress, CancellationToken token) { const int bufferSize = 1024 * 1024; await using var input = new FileStream(src, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true); await using var output = new FileStream(dst, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true); var total = input.Length; var copied = 0L; var buffer = new byte[bufferSize]; while (true) { var read = await input.ReadAsync(buffer, token).ConfigureAwait(false); if (read <= 0) { break; } await output.WriteAsync(buffer.AsMemory(0, read), token).ConfigureAwait(false); copied += read; if (total > 0) { var frac = copied / (double)total; var extra = frac >= 1.0 ? 9 : (int)Math.Floor(frac * 10.0); extra = Math.Clamp(extra, 0, 9); progress?.Report(90 + extra); } } } }