Skip to content

Commit

Permalink
поддержка указания произвольной папки с бэкапами в приложении командн…
Browse files Browse the repository at this point in the history
…ой строки
  • Loading branch information
miegir committed Mar 6, 2024
1 parent 2450014 commit 966daa5
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 53 deletions.
46 changes: 25 additions & 21 deletions ShibuyaTools.Core/FileSource.cs
@@ -1,43 +1,47 @@
namespace ShibuyaTools.Core;

public sealed class FileSource(string sourcePath)
public sealed class FileSource(string sourcePath, string backupPath)
{
private readonly string backupPath = sourcePath + ".bak";

private string ReadPath => File.Exists(backupPath) ? backupPath : sourcePath;
public string FileName => Path.GetFileName(sourcePath);
public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(sourcePath);
public FileDestination Destination => new(sourcePath);
public DateTime LastWriteTimeUtc => File.GetLastWriteTimeUtc(ReadPath);

private string ReadPath => File.Exists(backupPath) ? backupPath : sourcePath;

public FileStream OpenRead() => File.OpenRead(ReadPath);
public bool CanUnroll() => File.Exists(backupPath);
public FileTarget CreateTarget() => new(sourcePath, createBackupIfNotExists: true);

public void Unroll()
public FileTarget CreateTarget(ProgressCallback<long> callback)
{
if (File.Exists(backupPath))
{
File.Move(backupPath, sourcePath, overwrite: true);
}
}
var createBackupIfNotExists = !Path.Exists(backupPath);

public static IEnumerable<FileSource> EnumerateFiles(string directory, string searchPattern)
{
foreach (var path in Directory.EnumerateFiles(directory, searchPattern))
if (createBackupIfNotExists)
{
yield return new FileSource(path);
// Backup source if backup directory is different
var sourceDir = Path.GetDirectoryName(sourcePath);
var backupDir = Path.GetDirectoryName(backupPath);

if (sourceDir != backupDir)
{
using var target = new FileTarget(backupPath);
using var source = File.OpenRead(sourcePath);
source.CopyTo(target.Stream, callback);
target.CopyFileInfo(sourcePath);
target.Commit();

createBackupIfNotExists = false; // already backed up
}
}

return new(sourcePath, createBackupIfNotExists);
}

public static IEnumerable<FileSource> EnumerateFiles(string directory, params string[] searchPatterns)
public void Unroll()
{
foreach (var searchPattern in searchPatterns)
if (File.Exists(backupPath))
{
foreach (var source in EnumerateFiles(directory, searchPattern))
{
yield return source;
}
File.Move(backupPath, sourcePath, overwrite: true);
}
}
}
19 changes: 19 additions & 0 deletions ShibuyaTools.Core/FileTarget.cs
Expand Up @@ -19,6 +19,25 @@ public FileTarget(string path, bool createBackupIfNotExists = false)
public FileStream Stream { get; }
public string Extension => Path.GetExtension(path);

internal void CopyFileInfo(string sourcePath)
{
var sourceInfo = new FileInfo(sourcePath);
var targetInfo = new FileInfo(temporaryPath);
if (sourceInfo.Exists && targetInfo.Exists)
{
targetInfo.CreationTimeUtc = sourceInfo.CreationTimeUtc;
targetInfo.LastWriteTimeUtc = sourceInfo.LastWriteTimeUtc;
targetInfo.LastAccessTimeUtc = sourceInfo.LastAccessTimeUtc;
targetInfo.Attributes = sourceInfo.Attributes;
targetInfo.IsReadOnly = sourceInfo.IsReadOnly;

if (OperatingSystem.IsLinux())
{
targetInfo.UnixFileMode = sourceInfo.UnixFileMode;
}
}
}

public void Commit()
{
if (disposed)
Expand Down
3 changes: 3 additions & 0 deletions ShibuyaTools.Core/ProgressCallback.cs
@@ -0,0 +1,3 @@
namespace ShibuyaTools.Core;

public delegate void ProgressCallback<T>(ProgressPayload<T> payload);
3 changes: 3 additions & 0 deletions ShibuyaTools.Core/ProgressPayload.cs
@@ -0,0 +1,3 @@
namespace ShibuyaTools.Core;

public readonly record struct ProgressPayload<T>(T Total, T Position);
24 changes: 24 additions & 0 deletions ShibuyaTools.Core/StreamExtensions.cs
Expand Up @@ -6,6 +6,30 @@ public static class StreamExtensions
{
private const int DefaultBufferSize = 32767;

public static void CopyTo(this Stream source, Stream target, ProgressCallback<long> callback)
{
var buffer = ArrayPool<byte>.Shared.Rent(DefaultBufferSize);

try
{
int read;
var total = source.Length;
var copied = 0L;

while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
{
target.Write(buffer, 0, read);

callback(new ProgressPayload<long>(
Total: total, Position: copied += read));
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

public static void CopyBytesTo(this Stream source, Stream target, long count)
{
var bufferSize = DefaultBufferSize;
Expand Down
9 changes: 7 additions & 2 deletions ShibuyaTools.Games/ShibuyaGame.cs
Expand Up @@ -5,7 +5,7 @@

namespace ShibuyaTools.Games;

public class ShibuyaGame(ILogger logger, string gamePath) : Game(logger)
public class ShibuyaGame(ILogger logger, string gamePath, string? backupDirectory = null) : Game(logger)
{
public override GameVersionInfo? FindVersionInfo()
{
Expand Down Expand Up @@ -40,8 +40,13 @@ protected override IEnumerable<IResource> EnumerateResources()
var gameDir = Path.GetDirectoryName(gamePath);
if (gameDir is not null)
{
foreach (var source in FileSource.EnumerateFiles(gameDir, "*.wad"))
var backupDir = backupDirectory ?? gameDir;

foreach (var path in Directory.EnumerateFiles(gameDir, "*.wad"))
{
var backupName = Path.GetFileName(path) + ".bak";
var backupPath = Path.Combine(backupDir, backupName);
var source = new FileSource(path, backupPath);
yield return new WadResource(logger, source);
}
}
Expand Down
74 changes: 60 additions & 14 deletions ShibuyaTools.Resources.Wad/WadArchive.cs
@@ -1,6 +1,6 @@
using System.Collections.Frozen;
using System.Diagnostics;
using System.Reflection.PortableExecutable;
using System.Text;
using Microsoft.Extensions.Logging;
using ShibuyaTools.Core;

Expand All @@ -9,18 +9,17 @@ namespace ShibuyaTools.Resources.Wad;
internal sealed class WadArchive : IDisposable
{
private readonly FileSource source;
private readonly Stream stream;
private readonly BinaryReader reader;
private readonly WadHeader header;
private readonly long dataOffset;
private readonly FrozenSet<string> existingFileNames;
private readonly List<TargetFile> targetFileSourceList = [];
private Stream? stream;

public WadArchive(FileSource source)
{
this.source = source;
stream = source.OpenRead();
reader = new BinaryReader(stream);
var stream = EnsureStream();
using var reader = CreateReader(stream);
header = WadHeader.Read(reader);
dataOffset = stream.Position;
existingFileNames = header.Files
Expand All @@ -32,20 +31,22 @@ public WadArchive(FileSource source)

public void Dispose()
{
reader.Dispose();
stream.Dispose();
Close();
}

public bool Exists(string name) => existingFileNames.Contains(name);

public byte[] Read(WadFile file)
{
var stream = EnsureStream();
using var reader = CreateReader(stream);
stream.Position = dataOffset + file.Offset;
return reader.ReadBytes((int)file.Size);
}

public void Export(WadFile file, string path)
{
var stream = EnsureStream();
using var target = new FileTarget(path);
stream.Position = dataOffset + file.Offset;
stream.CopyBytesTo(target.Stream, file.Size);
Expand Down Expand Up @@ -87,22 +88,32 @@ public void Save(ILogger logger)
}

var targetHeader = header with { Files = targetFiles };
using var target = source.CreateTarget();
using var writer = new BinaryWriter(target.Stream);

Close();

logger.LogInformation("creating target...");
var stopwatch = Stopwatch.StartNew();
var scope = logger.BeginScope("creating target");
using var target = source.CreateTarget(ReportProgress);
scope?.Dispose();
logger.LogInformation("writing header...");

using var writer = new BinaryWriter(target.Stream);
targetHeader.WriteTo(writer);
writer.Flush();

var totalLength = FormatLength(target.Stream.Position + targetFileSourceList.Sum(file => file.Body.GetLength()));
var stopwatch = Stopwatch.StartNew();
var totalLength = target.Stream.Position + targetFileSourceList.Sum(file => file.Body.GetLength());

logger.LogInformation("writing wad...");
logger.LogInformation("writing content...");
scope = logger.BeginScope("writing content");
stopwatch.Restart();

foreach (var file in targetFileSourceList)
{
switch (file.Body)
{
case TargetBody.Internal(var offset, var length):
var stream = EnsureStream();
stream.Position = offset;
stream.CopyBytesTo(target.Stream, length);
break;
Expand All @@ -121,14 +132,32 @@ public void Save(ILogger logger)

if (stopwatch.Elapsed.TotalSeconds > 1)
{
logger.LogDebug("written {count} of {total}", FormatLength(target.Stream.Position), totalLength);
ReportProgress(new ProgressPayload<long>(
Total: totalLength,
Position: target.Stream.Position));

stopwatch.Restart();
}
}

scope?.Dispose();
logger.LogDebug("writing wad done.");
stream.Close();
Close();
target.Commit();

void ReportProgress(ProgressPayload<long> progress)
{
if (stopwatch.Elapsed.TotalSeconds > 1)
{
logger.LogDebug(
"written {count} of {total} ({progress:0.00}%)",
FormatLength(progress.Position),
FormatLength(progress.Total),
progress.Position * 100.0 / progress.Total);

stopwatch.Restart();
}
}
}

private static string FormatLength(float length)
Expand All @@ -141,6 +170,23 @@ private static string FormatLength(float length)
return $"{length:0.00}GB";
}

private Stream EnsureStream() => stream ??= source.OpenRead();

private static BinaryReader CreateReader(Stream stream)
{
return new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
}

private void Close()
{
var disposable = stream;
if (disposable is not null)
{
stream = null;
disposable?.Dispose();
}
}

private record TargetFile(string Name, TargetBody Body);

private abstract record TargetBody
Expand Down
23 changes: 15 additions & 8 deletions ShibuyaTools/CreateCommand.cs
@@ -1,8 +1,8 @@
using McMaster.Extensions.CommandLineUtils;
using System.ComponentModel.DataAnnotations;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using ShibuyaTools.Core;
using ShibuyaTools.Games;
using System.ComponentModel.DataAnnotations;

namespace ShibuyaTools;

Expand All @@ -25,6 +25,9 @@ internal class CreateCommand(ILogger<CreateCommand> logger)
[Option("-j|--object-directory")]
public string ObjectDirectory { get; }

[Option("-b|--backup-directory")]
public string BackupDirectory { get; }

[Required]
[LegalFilePath]
[Option("-a|--archive-path")]
Expand All @@ -46,12 +49,16 @@ public void OnExecute()

var sink = new MusterSink(logger);

new ShibuyaGame(logger, GamePath)
.Muster(new MusterArguments(
Sink: sink,
SourceDirectory: SourceDirectory,
ObjectDirectory: ObjectDirectory,
ForceObjects: Force || ForceObjects));
var game = new ShibuyaGame(
logger: logger,
gamePath: GamePath,
backupDirectory: BackupDirectory);

game.Muster(new MusterArguments(
Sink: sink,
SourceDirectory: SourceDirectory,
ObjectDirectory: ObjectDirectory,
ForceObjects: Force || ForceObjects));

sink.Pack(new PackArguments(
ArchivePath: ArchivePath,
Expand Down
13 changes: 11 additions & 2 deletions ShibuyaTools/ExportCommand.cs
Expand Up @@ -20,6 +20,9 @@ internal class ExportCommand(ILogger<ExportCommand> logger)
[Option("-e|--export-directory")]
public string ExportDirectory { get; }

[Option("-b|--backup-directory")]
public string BackupDirectory { get; }

[Option("-f|--force")]
public bool Force { get; }

Expand All @@ -31,8 +34,14 @@ public void OnExecute()
{
logger.LogInformation("executing...");

new ShibuyaGame(logger, GamePath)
.Export(new ExportArguments(ExportDirectory, Force: Force || ForceExport));
var game = new ShibuyaGame(
logger: logger,
gamePath: GamePath,
backupDirectory: BackupDirectory);

game.Export(new ExportArguments(
ExportDirectory: ExportDirectory,
Force: Force || ForceExport));

logger.LogInformation("executed.");
}
Expand Down

0 comments on commit 966daa5

Please sign in to comment.