diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2925937..5ac1cf3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,16 @@ All notable user-facing changes are documented here.
## [Unreleased]
+## [0.1.5] - 2026-08-02
+
+### Fixed
+
+- Update downloads now close their file handles before promoting verified packages or replacing an invalid cached package, preventing Windows file-lock errors during one-click updates.
+
+### Upgrade note
+
+- Captail 0.1.3 and 0.1.4 cannot complete an in-app update because the bug is inside those installed versions. Download and run the 0.1.5 Setup EXE once; later in-app updates will work normally.
+
## [0.1.3] - 2026-07-26
### Added
diff --git a/src/Captail/Captail.csproj b/src/Captail/Captail.csproj
index fbd530c..111a60e 100644
--- a/src/Captail/Captail.csproj
+++ b/src/Captail/Captail.csproj
@@ -14,7 +14,7 @@
Captail
Captail
Assets\Captail.ico
- 0.1.4
+ 0.1.5
true
diff --git a/src/Captail/UpdateService.cs b/src/Captail/UpdateService.cs
index 39bc664..89ebeb4 100644
--- a/src/Captail/UpdateService.cs
+++ b/src/Captail/UpdateService.cs
@@ -422,16 +422,21 @@ private static async Task DownloadVerifiedAsync(
if (File.Exists(destinationPath) &&
new FileInfo(destinationPath).Length == asset.Size)
{
- await using var cachedFile = new FileStream(
- destinationPath,
- FileMode.Open,
- FileAccess.Read,
- FileShare.Read,
- 128 * 1024,
- FileOptions.Asynchronous | FileOptions.SequentialScan);
- byte[] cachedHash = await SHA256.HashDataAsync(
- cachedFile,
- cancellationToken);
+ byte[] cachedHash;
+ await using (var cachedFile = new FileStream(
+ destinationPath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read,
+ 128 * 1024,
+ FileOptions.Asynchronous |
+ FileOptions.SequentialScan))
+ {
+ cachedHash = await SHA256.HashDataAsync(
+ cachedFile,
+ cancellationToken);
+ }
+
if (CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(
Convert.ToHexString(cachedHash)
@@ -460,66 +465,70 @@ private static async Task DownloadVerifiedAsync(
"Downloaded update size does not match release metadata.");
}
- await using Stream source =
- await response.Content.ReadAsStreamAsync(cancellationToken);
- await using var destination = new FileStream(
- temporaryPath,
- FileMode.Create,
- FileAccess.Write,
- FileShare.None,
- 128 * 1024,
- FileOptions.Asynchronous | FileOptions.SequentialScan);
- using IncrementalHash hash =
- IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
-
- byte[] buffer = new byte[128 * 1024];
- long total = 0;
- int lastProgress = -1;
- while (true)
+ await using (Stream source =
+ await response.Content.ReadAsStreamAsync(
+ cancellationToken))
+ await using (var destination = new FileStream(
+ temporaryPath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None,
+ 128 * 1024,
+ FileOptions.Asynchronous |
+ FileOptions.SequentialScan))
{
- int read = await source.ReadAsync(
- buffer,
- cancellationToken);
- if (read == 0)
- break;
- total += read;
- if (total > asset.Size || total > MaximumAssetBytes)
+ using IncrementalHash hash =
+ IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
+
+ byte[] buffer = new byte[128 * 1024];
+ long total = 0;
+ int lastProgress = -1;
+ while (true)
+ {
+ int read = await source.ReadAsync(
+ buffer,
+ cancellationToken);
+ if (read == 0)
+ break;
+ total += read;
+ if (total > asset.Size || total > MaximumAssetBytes)
+ {
+ throw new InvalidDataException(
+ "Downloaded update exceeds expected size.");
+ }
+ hash.AppendData(buffer, 0, read);
+ await destination.WriteAsync(
+ buffer.AsMemory(0, read),
+ cancellationToken);
+
+ int percent = (int)Math.Clamp(
+ total * 100L / asset.Size,
+ 0,
+ 100);
+ if (percent != lastProgress)
+ {
+ progress?.Report(percent);
+ lastProgress = percent;
+ }
+ }
+ await destination.FlushAsync(cancellationToken);
+
+ if (total != asset.Size)
{
throw new InvalidDataException(
- "Downloaded update exceeds expected size.");
+ "Downloaded update is incomplete.");
}
- hash.AppendData(buffer, 0, read);
- await destination.WriteAsync(
- buffer.AsMemory(0, read),
- cancellationToken);
-
- int percent = (int)Math.Clamp(
- total * 100L / asset.Size,
- 0,
- 100);
- if (percent != lastProgress)
+ string actualHash =
+ Convert.ToHexString(hash.GetHashAndReset())
+ .ToLowerInvariant();
+ if (!CryptographicOperations.FixedTimeEquals(
+ Encoding.ASCII.GetBytes(actualHash),
+ Encoding.ASCII.GetBytes(expectedHash)))
{
- progress?.Report(percent);
- lastProgress = percent;
+ throw new InvalidDataException(
+ "Downloaded update failed SHA-256 verification.");
}
}
- await destination.FlushAsync(cancellationToken);
-
- if (total != asset.Size)
- {
- throw new InvalidDataException(
- "Downloaded update is incomplete.");
- }
- string actualHash =
- Convert.ToHexString(hash.GetHashAndReset())
- .ToLowerInvariant();
- if (!CryptographicOperations.FixedTimeEquals(
- Encoding.ASCII.GetBytes(actualHash),
- Encoding.ASCII.GetBytes(expectedHash)))
- {
- throw new InvalidDataException(
- "Downloaded update failed SHA-256 verification.");
- }
File.Move(temporaryPath, destinationPath, overwrite: true);
progress?.Report(100);