2020-05-09 22:16:16 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Drawing;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Wabbajack.Common;
|
|
|
|
|
|
|
|
|
|
namespace Wabbajack.Lib.AuthorApi
|
|
|
|
|
{
|
|
|
|
|
public class Client
|
|
|
|
|
{
|
|
|
|
|
public static async Task<Client> Create(string? apiKey = null)
|
|
|
|
|
{
|
|
|
|
|
var client = await GetAuthorizedClient(apiKey);
|
|
|
|
|
return new Client(client);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-26 17:08:30 +00:00
|
|
|
|
private Client(Wabbajack.Lib.Http.Client client)
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
|
|
|
|
_client = client;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-26 17:08:30 +00:00
|
|
|
|
public static async Task<Wabbajack.Lib.Http.Client> GetAuthorizedClient(string? apiKey = null)
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
2020-06-26 17:08:30 +00:00
|
|
|
|
var client = new Wabbajack.Lib.Http.Client();
|
2020-05-09 22:16:16 +00:00
|
|
|
|
client.Headers.Add(("X-API-KEY", await GetAPIKey(apiKey)));
|
|
|
|
|
return client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string? ApiKeyOverride = null;
|
2020-06-26 17:08:30 +00:00
|
|
|
|
private Wabbajack.Lib.Http.Client _client;
|
2020-05-09 22:16:16 +00:00
|
|
|
|
|
|
|
|
|
public static async ValueTask<string> GetAPIKey(string? apiKey = null)
|
|
|
|
|
{
|
|
|
|
|
return apiKey ?? (await Consts.LocalAppDataPath.Combine(Consts.AuthorAPIKeyFile).ReadAllTextAsync()).Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-08-05 00:34:09 +00:00
|
|
|
|
public static async Task<CDNFileDefinition> GenerateFileDefinition(WorkQueue queue, AbsolutePath path, Action<string, Percent> progressFn)
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
|
|
|
|
IEnumerable<CDNFilePartDefinition> Blocks(AbsolutePath path)
|
|
|
|
|
{
|
|
|
|
|
var size = path.Size;
|
2021-07-17 05:32:37 +00:00
|
|
|
|
for (long block = 0; block * Consts.UploadedFileBlockSize < size; block ++)
|
2020-05-09 22:16:16 +00:00
|
|
|
|
yield return new CDNFilePartDefinition
|
|
|
|
|
{
|
|
|
|
|
Index = block,
|
2021-07-17 05:32:37 +00:00
|
|
|
|
Size = Math.Min(Consts.UploadedFileBlockSize, size - block * Consts.UploadedFileBlockSize),
|
|
|
|
|
Offset = block * Consts.UploadedFileBlockSize
|
2020-05-09 22:16:16 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var parts = Blocks(path).ToArray();
|
|
|
|
|
var definition = new CDNFileDefinition
|
|
|
|
|
{
|
|
|
|
|
OriginalFileName = path.FileName,
|
|
|
|
|
Size = path.Size,
|
2021-01-09 19:04:11 +00:00
|
|
|
|
Hash = await path.FileHashCachedAsync() ?? Hash.Empty,
|
2020-05-09 22:16:16 +00:00
|
|
|
|
Parts = await parts.PMap(queue, async part =>
|
|
|
|
|
{
|
|
|
|
|
progressFn("Hashing file parts", Percent.FactoryPutInRange(part.Index, parts.Length));
|
|
|
|
|
var buffer = new byte[part.Size];
|
2020-05-25 14:31:56 +00:00
|
|
|
|
await using (var fs = await path.OpenShared())
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
|
|
|
|
fs.Position = part.Offset;
|
|
|
|
|
await fs.ReadAsync(buffer);
|
|
|
|
|
}
|
|
|
|
|
part.Hash = buffer.xxHash();
|
|
|
|
|
return part;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return definition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Uri> UploadFile(WorkQueue queue, AbsolutePath path, Action<string, Percent> progressFn)
|
|
|
|
|
{
|
|
|
|
|
var definition = await GenerateFileDefinition(queue, path, progressFn);
|
|
|
|
|
|
2021-03-19 22:59:38 +00:00
|
|
|
|
await CircuitBreaker.WithAutoRetryAllAsync(async () =>
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
2021-03-19 22:59:38 +00:00
|
|
|
|
using var result = await _client.PutAsync($"{Consts.WabbajackBuildServerUri}authored_files/create",
|
|
|
|
|
new StringContent(definition.ToJson()));
|
2020-05-09 22:16:16 +00:00
|
|
|
|
progressFn("Starting upload", Percent.Zero);
|
|
|
|
|
definition.ServerAssignedUniqueId = await result.Content.ReadAsStringAsync();
|
2021-03-19 22:59:38 +00:00
|
|
|
|
});
|
2020-05-09 22:16:16 +00:00
|
|
|
|
|
|
|
|
|
var results = await definition.Parts.PMap(queue, async part =>
|
|
|
|
|
{
|
|
|
|
|
progressFn("Uploading Part", Percent.FactoryPutInRange(part.Index, definition.Parts.Length));
|
|
|
|
|
var buffer = new byte[part.Size];
|
2020-05-25 14:31:56 +00:00
|
|
|
|
await using (var fs = await path.OpenShared())
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
|
|
|
|
fs.Position = part.Offset;
|
|
|
|
|
await fs.ReadAsync(buffer);
|
|
|
|
|
}
|
2021-03-19 22:59:38 +00:00
|
|
|
|
|
|
|
|
|
return await CircuitBreaker.WithAutoRetryAllAsync(async () =>
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
2021-03-19 22:59:38 +00:00
|
|
|
|
using var putResult = await _client.PutAsync(
|
|
|
|
|
$"{Consts.WabbajackBuildServerUri}authored_files/{definition.ServerAssignedUniqueId}/part/{part.Index}",
|
|
|
|
|
new ByteArrayContent(buffer));
|
|
|
|
|
var hash = Hash.FromBase64(await putResult.Content.ReadAsStringAsync());
|
|
|
|
|
if (hash != part.Hash)
|
|
|
|
|
throw new InvalidDataException("Hashes don't match");
|
|
|
|
|
return hash;
|
|
|
|
|
});
|
|
|
|
|
|
2020-05-09 22:16:16 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
progressFn("Finalizing upload", Percent.Zero);
|
2021-03-19 22:59:38 +00:00
|
|
|
|
return await CircuitBreaker.WithAutoRetryAllAsync(async () =>
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
2021-03-19 22:59:38 +00:00
|
|
|
|
using var result = await _client.PutAsync(
|
|
|
|
|
$"{Consts.WabbajackBuildServerUri}authored_files/{definition.ServerAssignedUniqueId}/finish",
|
|
|
|
|
new StringContent(definition.ToJson()));
|
2020-05-09 22:16:16 +00:00
|
|
|
|
progressFn("Finished", Percent.One);
|
|
|
|
|
return new Uri(await result.Content.ReadAsStringAsync());
|
2021-03-19 22:59:38 +00:00
|
|
|
|
});
|
2020-05-09 22:16:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|