2025-01-05 17:05:23 +03:00
|
|
|
|
using System.Buffers.Binary;
|
2025-12-06 23:25:25 +03:00
|
|
|
|
using System.Diagnostics;
|
2026-01-16 21:02:34 +03:00
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2024-12-27 19:15:33 +03:00
|
|
|
|
using System.Globalization;
|
2026-01-16 21:02:34 +03:00
|
|
|
|
using System.IO.Compression;
|
2024-12-27 19:15:33 +03:00
|
|
|
|
using System.Net.Http.Headers;
|
|
|
|
|
|
using System.Numerics;
|
2025-03-13 14:58:47 +03:00
|
|
|
|
using Nebula.Shared.FileApis;
|
2025-01-05 17:05:23 +03:00
|
|
|
|
using Nebula.Shared.FileApis.Interfaces;
|
|
|
|
|
|
using Nebula.Shared.Models;
|
|
|
|
|
|
using Nebula.Shared.Utils;
|
2026-01-16 21:02:34 +03:00
|
|
|
|
using Robust.LoaderApi;
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-01-05 17:05:23 +03:00
|
|
|
|
namespace Nebula.Shared.Services;
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
|
|
|
|
|
public partial class ContentService
|
|
|
|
|
|
{
|
2025-05-02 20:06:33 +03:00
|
|
|
|
public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content");
|
|
|
|
|
|
public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest");
|
2026-01-16 21:02:34 +03:00
|
|
|
|
public readonly IReadWriteFileApi ZipContentApi = fileService.CreateFileApi("zipContent");
|
2025-07-02 21:32:51 +03:00
|
|
|
|
|
|
|
|
|
|
public void SetServerHash(string address, string hash)
|
|
|
|
|
|
{
|
|
|
|
|
|
var dict = varService.GetConfigValue(CurrentConVar.ServerManifestHash)!;
|
|
|
|
|
|
if (dict.TryGetValue(address, out var oldHash))
|
|
|
|
|
|
{
|
|
|
|
|
|
if(oldHash == hash) return;
|
|
|
|
|
|
|
|
|
|
|
|
ManifestFileApi.Remove(oldHash);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dict[address] = hash;
|
|
|
|
|
|
varService.SetConfigValue(CurrentConVar.ServerManifestHash, dict);
|
|
|
|
|
|
}
|
2025-05-02 20:06:33 +03:00
|
|
|
|
|
2025-07-02 21:32:51 +03:00
|
|
|
|
public HashApi CreateHashApi(List<RobustManifestItem> manifestItems)
|
2024-12-27 19:15:33 +03:00
|
|
|
|
{
|
2025-07-02 21:32:51 +03:00
|
|
|
|
return new HashApi(manifestItems, ContentFileApi);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
2026-01-16 21:02:34 +03:00
|
|
|
|
|
|
|
|
|
|
public async Task<IFileApi> EnsureItems(RobustBuildInfo info, ILoadingHandlerFactory loadingFactory,
|
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (info.RobustManifestInfo.HasValue)
|
|
|
|
|
|
return await EnsureItems(info.RobustManifestInfo.Value, loadingFactory, cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
if (info.DownloadUri.HasValue)
|
|
|
|
|
|
return await EnsureItems(info.DownloadUri.Value, loadingFactory, cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
throw new InvalidOperationException("DownloadUri is null");
|
|
|
|
|
|
}
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2026-01-16 21:02:34 +03:00
|
|
|
|
private async Task<HashApi> EnsureItems(ManifestReader manifestReader, Uri downloadUri,
|
2025-12-06 23:25:25 +03:00
|
|
|
|
ILoadingHandlerFactory loadingFactory,
|
2024-12-27 19:15:33 +03:00
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
List<RobustManifestItem> allItems = [];
|
|
|
|
|
|
|
|
|
|
|
|
while (manifestReader.TryReadItem(out var item))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
2025-03-13 14:58:47 +03:00
|
|
|
|
throw new TaskCanceledException();
|
|
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
allItems.Add(item.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-02 21:32:51 +03:00
|
|
|
|
var hashApi = CreateHashApi(allItems);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-12-06 23:25:25 +03:00
|
|
|
|
var items = allItems.Where(a=> !hashApi.Has(a)).ToList();
|
|
|
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
|
_logger.Log("Download Count:" + items.Count);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
await Download(downloadUri, items, hashApi, loadingFactory, cancellationToken);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-03-13 14:58:47 +03:00
|
|
|
|
return hashApi;
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 21:02:34 +03:00
|
|
|
|
private async Task<ZipFileApi> EnsureItems(RobustZipContentInfo info, ILoadingHandlerFactory loadingFactory, CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (TryFromFile(ZipContentApi, info.Hash, out var zipFile))
|
|
|
|
|
|
return zipFile;
|
|
|
|
|
|
|
|
|
|
|
|
var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater());
|
|
|
|
|
|
|
|
|
|
|
|
var response = await _http.GetAsync(info.DownloadUri, cancellationToken);
|
|
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
|
|
|
|
|
loadingHandler.SetLoadingMessage("Downloading zip content");
|
|
|
|
|
|
loadingHandler.SetJobsCount(response.Content.Headers.ContentLength ?? 0);
|
|
|
|
|
|
await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
|
|
|
|
ZipContentApi.Save(info.Hash, streamContent, loadingHandler);
|
|
|
|
|
|
loadingHandler.Dispose();
|
|
|
|
|
|
|
|
|
|
|
|
if (TryFromFile(ZipContentApi, info.Hash, out zipFile))
|
|
|
|
|
|
return zipFile;
|
|
|
|
|
|
|
|
|
|
|
|
ZipContentApi.Remove(info.Hash);
|
|
|
|
|
|
throw new Exception("Failed to load zip file");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool TryFromFile(IFileApi fileApi, string path, [NotNullWhen(true)] out ZipFileApi? zipFileApi)
|
|
|
|
|
|
{
|
|
|
|
|
|
zipFileApi = null;
|
|
|
|
|
|
if(!fileApi.TryOpen(path, out var zipContent))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
var zip = new ZipArchive(zipContent);
|
|
|
|
|
|
zipFileApi = new ZipFileApi(zip, null);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<HashApi> EnsureItems(RobustManifestInfo info, ILoadingHandlerFactory loadingFactory,
|
2024-12-27 19:15:33 +03:00
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
|
_logger.Log("Getting manifest: " + info.Hash);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater());
|
|
|
|
|
|
loadingHandler.SetLoadingMessage("Loading manifest");
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-05-02 20:06:33 +03:00
|
|
|
|
if (ManifestFileApi.TryOpen(info.Hash, out var stream))
|
2024-12-27 19:15:33 +03:00
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
_logger.Log("Loading manifest from disk");
|
|
|
|
|
|
loadingHandler.Dispose();
|
|
|
|
|
|
return await EnsureItems(new ManifestReader(stream), info.DownloadUri, loadingFactory, cancellationToken);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
2025-07-02 21:32:51 +03:00
|
|
|
|
|
|
|
|
|
|
SetServerHash(info.ManifestUri.ToString(), info.Hash);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
|
_logger.Log("Fetching manifest from: " + info.ManifestUri);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
loadingHandler.SetLoadingMessage("Fetching manifest from: " + info.ManifestUri.Host);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
|
|
|
|
|
var response = await _http.GetAsync(info.ManifestUri, cancellationToken);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
|
|
|
|
|
loadingHandler.SetJobsCount(response.Content.Headers.ContentLength ?? 0);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
ManifestFileApi.Save(info.Hash, streamContent, loadingHandler);
|
|
|
|
|
|
loadingHandler.Dispose();
|
2024-12-27 19:15:33 +03:00
|
|
|
|
streamContent.Seek(0, SeekOrigin.Begin);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
using var manifestReader = new ManifestReader(streamContent);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
return await EnsureItems(manifestReader, info.DownloadUri, loadingFactory, cancellationToken);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 21:02:34 +03:00
|
|
|
|
public void Unpack(IFileApi hashApi, IWriteFileApi otherApi, ILoadingHandler loadingHandler)
|
2024-12-27 19:15:33 +03:00
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
|
_logger.Log("Unpack manifest files");
|
2026-01-16 21:02:34 +03:00
|
|
|
|
var items = hashApi.AllFiles.ToList();
|
2025-01-08 18:00:06 +03:00
|
|
|
|
loadingHandler.AppendJob(items.Count);
|
2025-04-29 21:56:21 +03:00
|
|
|
|
|
|
|
|
|
|
var options = new ParallelOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
MaxDegreeOfParallelism = 10
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Parallel.ForEach(items, options, item =>
|
2025-01-08 18:00:06 +03:00
|
|
|
|
{
|
2025-03-13 14:58:47 +03:00
|
|
|
|
if (hashApi.TryOpen(item, out var stream))
|
2024-12-27 19:15:33 +03:00
|
|
|
|
{
|
2026-01-16 21:02:34 +03:00
|
|
|
|
_logger.Log($"Unpack {item}");
|
|
|
|
|
|
otherApi.Save(item, stream);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
stream.Close();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-01-16 21:02:34 +03:00
|
|
|
|
_logger.Error($"Error while unpacking thinks {item}");
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
2025-01-08 18:00:06 +03:00
|
|
|
|
loadingHandler.AppendResolvedJob();
|
2025-04-29 21:56:21 +03:00
|
|
|
|
});
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 23:25:25 +03:00
|
|
|
|
private async Task Download(Uri contentCdn, List<RobustManifestItem> toDownload, HashApi hashApi, ILoadingHandlerFactory loadingHandlerFactory,
|
2025-01-14 22:10:16 +03:00
|
|
|
|
CancellationToken cancellationToken)
|
2024-12-27 19:15:33 +03:00
|
|
|
|
{
|
|
|
|
|
|
if (toDownload.Count == 0 || cancellationToken.IsCancellationRequested)
|
|
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
_logger.Log("Nothing to download! Skip!");
|
2024-12-27 19:15:33 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
|
_logger.Log("Downloading from: " + contentCdn);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
|
|
|
|
|
var requestBody = new byte[toDownload.Count * 4];
|
|
|
|
|
|
var reqI = 0;
|
|
|
|
|
|
foreach (var item in toDownload)
|
|
|
|
|
|
{
|
|
|
|
|
|
BinaryPrimitives.WriteInt32LittleEndian(requestBody.AsSpan(reqI, 4), item.Id);
|
|
|
|
|
|
reqI += 4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, contentCdn);
|
|
|
|
|
|
request.Headers.Add(
|
|
|
|
|
|
"X-Robust-Download-Protocol",
|
2025-01-14 22:10:16 +03:00
|
|
|
|
varService.GetConfigValue(CurrentConVar.ManifestDownloadProtocolVersion)
|
|
|
|
|
|
.ToString(CultureInfo.InvariantCulture));
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
|
|
|
|
|
request.Content = new ByteArrayContent(requestBody);
|
|
|
|
|
|
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
|
|
|
|
|
|
|
|
|
|
|
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("zstd"));
|
|
|
|
|
|
var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
|
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
2025-12-06 23:25:25 +03:00
|
|
|
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
if (response.Content.Headers.ContentEncoding.Contains("zstd"))
|
|
|
|
|
|
stream = new ZStdDecompressStream(stream);
|
|
|
|
|
|
|
|
|
|
|
|
await using var streamDispose = stream;
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
|
|
|
|
|
var streamHeader = await stream.ReadExactAsync(4, cancellationToken);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
var streamFlags = (DownloadStreamHeaderFlags)BinaryPrimitives.ReadInt32LittleEndian(streamHeader);
|
|
|
|
|
|
var preCompressed = (streamFlags & DownloadStreamHeaderFlags.PreCompressed) != 0;
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
var compressContext = preCompressed ? null : new ZStdCCtx();
|
|
|
|
|
|
var decompressContext = preCompressed ? new ZStdDCtx() : null;
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
var fileHeader = new byte[preCompressed ? 8 : 4];
|
|
|
|
|
|
|
2025-12-06 23:25:25 +03:00
|
|
|
|
var downloadLoadHandler = loadingHandlerFactory.CreateLoadingContext();
|
|
|
|
|
|
downloadLoadHandler.SetJobsCount(toDownload.Count);
|
|
|
|
|
|
downloadLoadHandler.SetLoadingMessage("Fetching files...");
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-12-06 23:25:25 +03:00
|
|
|
|
if (loadingHandlerFactory is IConnectionSpeedHandler speedHandlerStart)
|
|
|
|
|
|
speedHandlerStart.PasteSpeed(0);
|
|
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var compressBuffer = new byte[1024];
|
|
|
|
|
|
var readBuffer = new byte[1024];
|
|
|
|
|
|
|
|
|
|
|
|
var i = 0;
|
2025-12-06 23:25:25 +03:00
|
|
|
|
var downloadWatchdog = new Stopwatch();
|
|
|
|
|
|
var lengthAcc = 0;
|
|
|
|
|
|
var timeAcc = 0L;
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
foreach (var item in toDownload)
|
|
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
|
|
downloadWatchdog.Restart();
|
|
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
// Read file header.
|
2025-12-06 23:25:25 +03:00
|
|
|
|
await stream.ReadExactAsync(fileHeader, cancellationToken);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
|
|
|
|
|
var length = BinaryPrimitives.ReadInt32LittleEndian(fileHeader.AsSpan(0, 4));
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
|
|
|
|
|
var fileLoadingHandler = loadingHandlerFactory.CreateLoadingContext(new FileLoadingFormater());
|
|
|
|
|
|
fileLoadingHandler.SetLoadingMessage(item.Path.Split("/").Last());
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-12-06 23:25:25 +03:00
|
|
|
|
var blockFileLoadHandle = length <= 100000;
|
|
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
|
EnsureBuffer(ref readBuffer, length);
|
|
|
|
|
|
var data = readBuffer.AsMemory(0, length);
|
|
|
|
|
|
|
|
|
|
|
|
if (preCompressed)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Compressed length from extended header.
|
|
|
|
|
|
var compressedLength = BinaryPrimitives.ReadInt32LittleEndian(fileHeader.AsSpan(4, 4));
|
|
|
|
|
|
|
|
|
|
|
|
if (compressedLength > 0)
|
|
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
fileLoadingHandler.AppendJob(compressedLength);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
EnsureBuffer(ref compressBuffer, compressedLength);
|
|
|
|
|
|
var compressedData = compressBuffer.AsMemory(0, compressedLength);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
await stream.ReadExactAsync(compressedData, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
|
|
|
|
|
// Decompress so that we can verify hash down below.
|
|
|
|
|
|
|
|
|
|
|
|
var decompressedLength = decompressContext!.Decompress(data.Span, compressedData.Span);
|
|
|
|
|
|
|
|
|
|
|
|
if (decompressedLength != data.Length)
|
|
|
|
|
|
throw new Exception($"Compressed blob {i} had incorrect decompressed size!");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
fileLoadingHandler.AppendJob(length);
|
|
|
|
|
|
await stream.ReadExactAsync(data, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
fileLoadingHandler.AppendJob(length);
|
|
|
|
|
|
await stream.ReadExactAsync(data, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using var fileStream = new MemoryStream(data.ToArray());
|
2025-12-06 23:25:25 +03:00
|
|
|
|
hashApi.Save(item, fileStream, null);
|
2024-12-27 19:15:33 +03:00
|
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
|
_logger.Log("file saved:" + item.Path);
|
2025-12-06 23:25:25 +03:00
|
|
|
|
fileLoadingHandler.Dispose();
|
|
|
|
|
|
downloadLoadHandler.AppendResolvedJob();
|
2024-12-27 19:15:33 +03:00
|
|
|
|
i += 1;
|
2025-12-06 23:25:25 +03:00
|
|
|
|
|
|
|
|
|
|
if (loadingHandlerFactory is not IConnectionSpeedHandler speedHandler)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (downloadWatchdog.ElapsedMilliseconds + timeAcc < 1000)
|
|
|
|
|
|
{
|
|
|
|
|
|
timeAcc += downloadWatchdog.ElapsedMilliseconds;
|
|
|
|
|
|
lengthAcc += length;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (timeAcc != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
timeAcc += downloadWatchdog.ElapsedMilliseconds;
|
|
|
|
|
|
lengthAcc += length;
|
|
|
|
|
|
|
|
|
|
|
|
speedHandler.PasteSpeed((int)(lengthAcc / (timeAcc / 1000)));
|
|
|
|
|
|
|
|
|
|
|
|
timeAcc = 0;
|
|
|
|
|
|
lengthAcc = 0;
|
|
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
speedHandler.PasteSpeed((int)(length / (downloadWatchdog.ElapsedMilliseconds / 1000)));
|
2024-12-27 19:15:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2025-12-06 23:25:25 +03:00
|
|
|
|
downloadLoadHandler.Dispose();
|
2024-12-27 19:15:33 +03:00
|
|
|
|
decompressContext?.Dispose();
|
|
|
|
|
|
compressContext?.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static void EnsureBuffer(ref byte[] buf, int needsFit)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (buf.Length >= needsFit)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
var newLen = 2 << BitOperations.Log2((uint)needsFit - 1);
|
|
|
|
|
|
|
|
|
|
|
|
buf = new byte[newLen];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|