using System.Numerics; using System.Security.Cryptography; namespace RadicadeServer.Storage; public sealed record StudioAssetUpload(string Filename, string Hash, long Length); public static class StudioAssetStorage { public static string Filename(byte[] digest) { var value = new BigInteger(digest, isUnsigned: true, isBigEndian: false); const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; var result = ""; while (value > 0) { value = BigInteger.DivRem(value, 36, out var remainder); result = alphabet[(int)remainder] + result; } return result.PadLeft(50, '0')[..40]; } public static async Task Save(IFormFile file, CancellationToken cancellation) { await using var input = file.OpenReadStream(); return await Save(input, cancellation); } public static async Task Save(Stream input, CancellationToken cancellation) { Directory.CreateDirectory(DBConfig.CDNStorage); var temporary = Path.Combine(DBConfig.CDNStorage, Guid.NewGuid() + ".uploading"); try { await using (var output = System.IO.File.Create(temporary)) await input.CopyToAsync(output, cancellation); var length = new FileInfo(temporary).Length; byte[] hash; await using (var storedFile = System.IO.File.OpenRead(temporary)) hash = await SHA256.HashDataAsync(storedFile, cancellation); var filename = Filename(hash); var destination = Path.Combine(DBConfig.CDNStorage, filename); try { System.IO.File.Move(temporary, destination); } catch (IOException) when (System.IO.File.Exists(destination)) { } return new StudioAssetUpload(filename, Convert.ToBase64String(hash), length); } finally { if (System.IO.File.Exists(temporary)) System.IO.File.Delete(temporary); } } }