diff --git a/EthernaGatewayCli.sln b/EthernaGatewayCli.sln index 5bc5540..908a296 100644 --- a/EthernaGatewayCli.sln +++ b/EthernaGatewayCli.sln @@ -19,6 +19,7 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug-DevEnv|Any CPU = Debug-DevEnv|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution @@ -26,6 +27,8 @@ Global {F348B863-6892-45AD-AC33-3AD833A14BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU {F348B863-6892-45AD-AC33-3AD833A14BBB}.Release|Any CPU.ActiveCfg = Release|Any CPU {F348B863-6892-45AD-AC33-3AD833A14BBB}.Release|Any CPU.Build.0 = Release|Any CPU + {F348B863-6892-45AD-AC33-3AD833A14BBB}.Debug-DevEnv|Any CPU.ActiveCfg = Debug-DevEnv|Any CPU + {F348B863-6892-45AD-AC33-3AD833A14BBB}.Debug-DevEnv|Any CPU.Build.0 = Debug-DevEnv|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {6DD03B58-3404-41AD-B84D-E43E600C54F3} = {D3B7E18B-46F8-4072-9D14-801FDB15868C} diff --git a/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommand.cs index bf5b88a..9bb0e17 100644 --- a/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommand.cs +++ b/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommand.cs @@ -16,38 +16,34 @@ using Etherna.CliHelper.Models.Commands; using Etherna.CliHelper.Services; using Etherna.GatewayCli.Services; +using Etherna.Sdk.Users.Gateway.Services; using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Linq; +using System.Net.Http; using System.Net.WebSockets; using System.Reflection; -using System.Threading; +using System.Text; using System.Threading.Tasks; namespace Etherna.GatewayCli.Commands.Etherna.Chunk { - public class UploadCommand : CommandBase + public class UploadCommand( + Assembly assembly, + IAuthenticationService authService, + IGatewayService gatewayService, + IIoService ioService, + IPostageBatchService postageBatchService, + IServiceProvider serviceProvider) + : CommandBase(assembly, ioService, serviceProvider) { // Consts. + private ushort ChunkBatchMaxSize = 500; private const int UploadMaxRetry = 10; private readonly TimeSpan UploadRetryTimeSpan = TimeSpan.FromSeconds(5); - - // Fields. - private readonly IAuthenticationService authService; - private readonly IGatewayService gatewayService; - // Constructor. - public UploadCommand( - Assembly assembly, - IAuthenticationService authService, - IGatewayService gatewayService, - IIoService ioService, - IServiceProvider serviceProvider) - : base(assembly, ioService, serviceProvider) - { - this.authService = authService; - this.gatewayService = gatewayService; - } - // Properties. public override string CommandArgsHelpString => "CHUNK_DIR"; public override string Description => "Upload chunks from a directory"; @@ -77,53 +73,98 @@ protected override async Task ExecuteAsync(string[] commandArgs) var batchDepth = postageBuckets.RequiredPostageBatchDepth; // Identify postage batch and tag to use. - var postageBatchId = await gatewayService.GetUsablePostageBatchIdAsync( + var batchId = await postageBatchService.GetUsablePostageBatchAsync( batchDepth, - Options.UsePostageBatchId is null ? (PostageBatchId?)null : new PostageBatchId(Options.UsePostageBatchId), Options.NewPostageTtl, Options.NewPostageAutoPurchase, + Options.UsePostageBatchId is null ? (PostageBatchId?)null : new PostageBatchId(Options.UsePostageBatchId), Options.NewPostageLabel); - var tagInfo = await gatewayService.CreateTagAsync(postageBatchId); //necessary to not bypass bee local storage - - // Upload with websocket. + + IoService.WriteLine($"Start uploading {chunkFiles.Length} chunks..."); + int totalUploaded = 0; - for (int i = 0; i < UploadMaxRetry && totalUploaded < chunkFiles.Length; i++) + var uploadStartDateTime = DateTime.UtcNow; + for (int retry = 0; retry < UploadMaxRetry && totalUploaded < chunkFiles.Length; retry++) { - // Create websocket. - using var chunkUploaderWs = await gatewayService.GetChunkUploaderWebSocketAsync(postageBatchId, tagInfo.Id); - try { - for (int j = totalUploaded; j < chunkFiles.Length; j++) + var lastUpdateDateTime = DateTime.UtcNow; + + // Iterate on chunk batches. + while(totalUploaded < chunkFiles.Length) { - var chunkPath = chunkFiles[j]; - var chunk = SwarmChunk.BuildFromSpanAndData( - Path.GetFileNameWithoutExtension(chunkPath), - await File.ReadAllBytesAsync(chunkPath)); + var now = DateTime.UtcNow; + if (now - lastUpdateDateTime > TimeSpan.FromSeconds(1)) + { + PrintProgressLine( + "Uploading chunks", + totalUploaded, + chunkFiles.Length, + uploadStartDateTime); + lastUpdateDateTime = now; + } + + var chunkBatchFiles = chunkFiles.Skip(totalUploaded).Take(ChunkBatchMaxSize).ToArray(); + + List chunkBatch = []; + foreach (var chunkFile in chunkBatchFiles) + chunkBatch.Add(SwarmChunk.BuildFromSpanAndData( + Path.GetFileNameWithoutExtension(chunkFile), + await File.ReadAllBytesAsync(chunkFile))); - await chunkUploaderWs.SendChunkAsync(chunk, CancellationToken.None); + await gatewayService.ChunksBulkUploadAsync( + chunkBatch.ToArray(), + batchId); + retry = 0; - totalUploaded++; + totalUploaded += chunkBatchFiles.Length; } + IoService.WriteLine(); } - catch (Exception e) when (e is WebSocketException or OperationCanceledException) + catch (Exception e) when ( + e is HttpRequestException + or InvalidOperationException + or WebSocketException + or OperationCanceledException) { IoService.WriteErrorLine($"Error uploading chunks"); IoService.WriteLine(e.ToString()); - if (i + 1 < UploadMaxRetry) + if (retry + 1 < UploadMaxRetry) { Console.WriteLine("Retry..."); await Task.Delay(UploadRetryTimeSpan); } } - finally - { - await chunkUploaderWs.CloseAsync(); - } } IoService.WriteLine($"Uploaded {totalUploaded} chunks successfully."); } + + private void PrintProgressLine(string message, long uploadedChunks, long totalChunks, DateTime startDateTime) + { + // Calculate ETA. + var elapsedTime = DateTime.UtcNow - startDateTime; + TimeSpan? eta = null; + var progressStatus = (double)uploadedChunks / totalChunks; + if (progressStatus != 0) + { + var totalRequiredTime = TimeSpan.FromSeconds(elapsedTime.TotalSeconds / progressStatus); + eta = totalRequiredTime - elapsedTime; + } + + // Print update. + var strBuilder = new StringBuilder(); + + strBuilder.Append(CultureInfo.InvariantCulture, + $"{message} ({(progressStatus * 100):N2}%) {uploadedChunks} chunks of {totalChunks}."); + + if (eta is not null) + strBuilder.Append(CultureInfo.InvariantCulture, $" ETA: {eta:hh\\:mm\\:ss}"); + + strBuilder.Append('\r'); + + IoService.Write(strBuilder.ToString()); + } } } \ No newline at end of file diff --git a/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommandOptions.cs b/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommandOptions.cs index ec9574a..c41afed 100644 --- a/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommandOptions.cs +++ b/src/EthernaGatewayCli/Commands/Etherna/Chunk/UploadCommandOptions.cs @@ -12,6 +12,7 @@ // You should have received a copy of the GNU Affero General Public License along with Etherna Gateway CLI. // If not, see . +using Etherna.BeeNet.Models; using Etherna.CliHelper.Models.Commands; using Etherna.CliHelper.Models.Commands.OptionRequirements; using System; diff --git a/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs index 1d58aad..aca05ad 100644 --- a/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs +++ b/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs @@ -16,31 +16,21 @@ using Etherna.CliHelper.Models.Commands; using Etherna.CliHelper.Services; using Etherna.GatewayCli.Services; +using Etherna.Sdk.Users.Gateway.Services; using System; using System.Reflection; using System.Threading.Tasks; namespace Etherna.GatewayCli.Commands.Etherna.Postage { - public class CreateCommand : CommandBase + public class CreateCommand( + Assembly assembly, + IAuthenticationService authService, + IGatewayService gatewayService, + IIoService ioService, + IServiceProvider serviceProvider) + : CommandBase(assembly, ioService, serviceProvider) { - // Fields. - private readonly IAuthenticationService authService; - private readonly IGatewayService gatewayService; - - // Constructor. - public CreateCommand( - Assembly assembly, - IAuthenticationService authService, - IGatewayService gatewayService, - IIoService ioService, - IServiceProvider serviceProvider) - : base(assembly, ioService, serviceProvider) - { - this.authService = authService; - this.gatewayService = gatewayService; - } - // Properties. public override string Description => "Create a new postage batch"; @@ -66,7 +56,14 @@ protected override async Task ExecuteAsync(string[] commandArgs) } else throw new InvalidOperationException("Amount or TTL are required"); - var batchId = await gatewayService.CreatePostageBatchAsync(amount, Options.Depth, Options.Label); + var batchId = await gatewayService.CreatePostageBatchAsync( + amount, + Options.Depth, + Options.Label, + onWaitingBatchCreation: () => IoService.Write("Waiting for batch created... (it may take a while)"), + onBatchCreated: _ => IoService.WriteLine(". Done"), + onWaitingBatchUsable: () => IoService.Write("Waiting for batch being usable... (it may take a while)"), + onBatchUsable: () => IoService.WriteLine(". Done")); // Print result. IoService.WriteLine(); diff --git a/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs index 4216a17..195ccf3 100644 --- a/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs +++ b/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs @@ -18,6 +18,7 @@ using Etherna.CliHelper.Services; using Etherna.GatewayCli.Services; using Etherna.Sdk.Gateway.GenClients; +using Etherna.Sdk.Users.Gateway.Services; using System; using System.Reflection; using System.Text.Json; @@ -25,7 +26,13 @@ namespace Etherna.GatewayCli.Commands.Etherna.Postage { - public class InfoCommand : CommandBase + public class InfoCommand( + Assembly assembly, + IAuthenticationService authService, + IGatewayService gatewayService, + IIoService ioService, + IServiceProvider serviceProvider) + : CommandBase(assembly, ioService, serviceProvider) { // Consts. private static readonly JsonSerializerOptions SerializerOptions = new() @@ -38,23 +45,6 @@ public class InfoCommand : CommandBase }, WriteIndented = true }; - - // Fields. - private readonly IAuthenticationService authService; - private readonly IGatewayService gatewayService; - - // Constructor. - public InfoCommand( - Assembly assembly, - IAuthenticationService authService, - IGatewayService gatewayService, - IIoService ioService, - IServiceProvider serviceProvider) - : base(assembly, ioService, serviceProvider) - { - this.authService = authService; - this.gatewayService = gatewayService; - } // Properties. public override string Description => "Get info about a postage batch"; diff --git a/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs index 2896277..5ec89ba 100644 --- a/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs +++ b/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs @@ -15,31 +15,21 @@ using Etherna.CliHelper.Models.Commands; using Etherna.CliHelper.Services; using Etherna.GatewayCli.Services; +using Etherna.Sdk.Users.Gateway.Services; using System; using System.Reflection; using System.Threading.Tasks; namespace Etherna.GatewayCli.Commands.Etherna.Resource { - public class FundCommand : CommandBase + public class FundCommand( + Assembly assembly, + IAuthenticationService authService, + IGatewayService gatewayService, + IIoService ioService, + IServiceProvider serviceProvider) + : CommandBase(assembly, ioService, serviceProvider) { - // Fields. - private readonly IAuthenticationService authService; - private readonly IGatewayService gatewayService; - - // Constructor. - public FundCommand( - Assembly assembly, - IAuthenticationService authService, - IGatewayService gatewayService, - IIoService ioService, - IServiceProvider serviceProvider) - : base(assembly, ioService, serviceProvider) - { - this.authService = authService; - this.gatewayService = gatewayService; - } - // Properties. public override string CommandArgsHelpString => "RESOURCE_ID"; public override string Description => "Fund resource budget"; diff --git a/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs index c469429..01b7626 100644 --- a/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs +++ b/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs @@ -16,6 +16,7 @@ using Etherna.CliHelper.Models.Commands; using Etherna.CliHelper.Services; using Etherna.GatewayCli.Services; +using Etherna.Sdk.Users.Gateway.Services; using System; using System.IO; using System.Reflection; @@ -23,32 +24,20 @@ namespace Etherna.GatewayCli.Commands.Etherna { - public class UploadCommand : CommandBase + public class UploadCommand( + Assembly assembly, + IAuthenticationService authService, + IFileService fileService, + IGatewayService gatewayService, + IIoService ioService, + IPostageBatchService postageBatchService, + IServiceProvider serviceProvider) + : CommandBase(assembly, ioService, serviceProvider) { // Consts. private const int UploadMaxRetry = 10; private readonly TimeSpan UploadRetryTimeSpan = TimeSpan.FromSeconds(5); - - // Fields. - private readonly IAuthenticationService authService; - private readonly IFileService fileService; - private readonly IGatewayService gatewayService; - // Constructor. - public UploadCommand( - Assembly assembly, - IAuthenticationService authService, - IFileService fileService, - IGatewayService gatewayService, - IIoService ioService, - IServiceProvider serviceProvider) - : base(assembly, ioService, serviceProvider) - { - this.authService = authService; - this.fileService = fileService; - this.gatewayService = gatewayService; - } - // Properties. public override string CommandArgsHelpString => "SOURCE [SOURCE ...]"; public override string Description => "Upload files and directories to Swarm"; @@ -67,14 +56,14 @@ protected override async Task ExecuteAsync(string[] commandArgs) await authService.SignInAsync(); // Search files and calculate required postage batch depth. - var batchDepth = await gatewayService.CalculatePostageBatchDepthAsync(paths); + var batchDepth = await postageBatchService.CalculatePostageBatchDepthAsync(paths); // Identify postage batch to use. - var postageBatchId = await gatewayService.GetUsablePostageBatchIdAsync( + var postageBatchId = await postageBatchService.GetUsablePostageBatchAsync( batchDepth, - Options.UsePostageBatchId is null ? (PostageBatchId?)null : new PostageBatchId(Options.UsePostageBatchId), Options.NewPostageTtl, Options.NewPostageAutoPurchase, + Options.UsePostageBatchId is null ? (PostageBatchId?)null : new PostageBatchId(Options.UsePostageBatchId), Options.NewPostageLabel); // Upload file. diff --git a/src/EthernaGatewayCli/EthernaGatewayCli.csproj b/src/EthernaGatewayCli/EthernaGatewayCli.csproj index 39a57c4..b3d5f18 100644 --- a/src/EthernaGatewayCli/EthernaGatewayCli.csproj +++ b/src/EthernaGatewayCli/EthernaGatewayCli.csproj @@ -24,6 +24,13 @@ COPYING true true + Debug;Debug-DevEnv;Release + AnyCPU + + + + TRACE;DEVENV + true @@ -32,8 +39,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/EthernaGatewayCli/Program.cs b/src/EthernaGatewayCli/Program.cs index 44458ed..2e3b4c2 100644 --- a/src/EthernaGatewayCli/Program.cs +++ b/src/EthernaGatewayCli/Program.cs @@ -85,6 +85,11 @@ public static async Task Main(string[] args) null, 11430, ApiScopes, +#if DEVENV + authority: "https://localhost:44379/", +#else + authority: EthernaUserClientsBuilder.DefaultSsoUrl, +#endif httpClientName: CommonConsts.HttpClientName, configureHttpClient: c => { @@ -96,17 +101,24 @@ public static async Task Main(string[] args) ethernaClientsBuilder = services.AddEthernaUserClientsWithApiKeyAuth( ethernaCommandOptions.ApiKey, ApiScopes, +#if DEVENV + authority: "https://localhost:44379/", +#else + authority: EthernaUserClientsBuilder.DefaultSsoUrl, +#endif httpClientName: CommonConsts.HttpClientName, configureHttpClient: c => { c.Timeout = TimeSpan.FromMinutes(30); }); } - if (ethernaCommandOptions.CustomGatewayUrl is null) - ethernaClientsBuilder.AddEthernaGatewayClient(); - else - ethernaClientsBuilder.AddEthernaGatewayClient( - ethernaCommandOptions.CustomGatewayUrl); + ethernaClientsBuilder.AddEthernaGatewayClient( +#if DEVENV + gatewayBaseUrl: ethernaCommandOptions.CustomGatewayUrl ?? "http://localhost:1633/" +#else + gatewayBaseUrl: ethernaCommandOptions.CustomGatewayUrl ?? EthernaUserClientsBuilder.DefaultGatewayUrl +#endif + ); var serviceProvider = services.BuildServiceProvider(); diff --git a/src/EthernaGatewayCli/ServiceCollectionExtensions.cs b/src/EthernaGatewayCli/ServiceCollectionExtensions.cs index a98c013..877100c 100644 --- a/src/EthernaGatewayCli/ServiceCollectionExtensions.cs +++ b/src/EthernaGatewayCli/ServiceCollectionExtensions.cs @@ -15,7 +15,8 @@ using Etherna.BeeNet.Services; using Etherna.CliHelper.Services; using Etherna.GatewayCli.Services; -using Etherna.GatewayCli.Services.Options; +using Etherna.Sdk.Users.Gateway.Options; +using Etherna.Sdk.Users.Gateway.Services; using Microsoft.Extensions.DependencyInjection; using System; using System.Reflection; @@ -37,6 +38,7 @@ public static void AddCoreServices( services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Add singleton services. services.AddSingleton(typeof(Program).GetTypeInfo().Assembly); diff --git a/src/EthernaGatewayCli/Services/GatewayService.cs b/src/EthernaGatewayCli/Services/GatewayService.cs deleted file mode 100644 index 623f504..0000000 --- a/src/EthernaGatewayCli/Services/GatewayService.cs +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright 2024-present Etherna SA -// This file is part of Etherna Gateway CLI. -// -// Etherna Gateway CLI is free software: you can redistribute it and/or modify it under the terms of the -// GNU Affero General Public License as published by the Free Software Foundation, -// either version 3 of the License, or (at your option) any later version. -// -// Etherna Gateway CLI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Etherna Gateway CLI. -// If not, see . - -using Etherna.BeeNet.Hashing.Postage; -using Etherna.BeeNet.Models; -using Etherna.BeeNet.Services; -using Etherna.CliHelper.Services; -using Etherna.GatewayCli.Services.Options; -using Etherna.Sdk.Gateway.GenClients; -using Etherna.Sdk.Users.Gateway.Clients; -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Etherna.GatewayCli.Services -{ - public class GatewayService( - IChunkService chunkService, - IEthernaUserGatewayClient ethernaGatewayClient, - IFileService fileService, - IIoService ioService, - IOptions options) - : IGatewayService - { - // Const. - private readonly TimeSpan BatchCheckTimeSpan = new(0, 0, 0, 5); - private readonly TimeSpan BatchCreationTimeout = new(0, 0, 10, 0); - private readonly TimeSpan BatchUsableTimeout = new(0, 0, 10, 0); - - // Fields. - private readonly GatewayServiceOptions options = options.Value; - - // Methods. - public async Task CalculatePostageBatchDepthAsync(Stream fileStream, string fileContentType, string fileName) => - (await chunkService.EvaluateSingleFileUploadAsync(fileStream, fileContentType, fileName)) - .PostageStampIssuer.Buckets.RequiredPostageBatchDepth; - - public async Task CalculatePostageBatchDepthAsync(byte[] fileData, string fileContentType, string fileName) => - (await chunkService.EvaluateSingleFileUploadAsync(fileData, fileContentType, fileName)) - .PostageStampIssuer.Buckets.RequiredPostageBatchDepth; - - [SuppressMessage("Performance", "CA1851:Possible multiple enumerations of \'IEnumerable\' collection")] - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - public async Task CalculatePostageBatchDepthAsync(IEnumerable paths) - { - ArgumentNullException.ThrowIfNull(paths, nameof(paths)); - if (!paths.Any()) - throw new ArgumentOutOfRangeException(nameof(paths), "Empty file paths"); - - ioService.Write("Calculating required postage batch depth... "); - - var stampIssuer = new PostageStampIssuer(PostageBatch.MaxDepthInstance); - UploadEvaluationResult lastResult = null!; - foreach (var path in paths) - { - if (File.Exists(path)) //is a file - { - await using var fileStream = File.OpenRead(path); - var mimeType = fileService.GetMimeType(path); - var fileName = Path.GetFileName(path); - - lastResult = await chunkService.EvaluateSingleFileUploadAsync( - fileStream, - mimeType, - fileName, - postageStampIssuer: stampIssuer); - } - else if (Directory.Exists(path)) //is a directory - { - lastResult = await chunkService.EvaluateDirectoryUploadAsync( - path, - postageStampIssuer: stampIssuer); - } - else //invalid path - throw new InvalidOperationException($"Path {path} is not valid"); - } - - ioService.WriteLine("Done"); - - return lastResult.PostageStampIssuer.Buckets.RequiredPostageBatchDepth; - } - - public async Task CreatePostageBatchAsync(BzzBalance amount, int batchDepth, string? label) - { - if (amount <= 0) - throw new ArgumentException("Amount must be positive"); - if (batchDepth < PostageBatch.MinDepth) - throw new ArgumentException($"Postage depth must be at least {PostageBatch.MinDepth}"); - - // Start creation. - var bzzPrice = PostageBatch.CalculatePrice(amount, batchDepth); - ioService.WriteLine($"Creating postage batch..."); - - PostageBatchId? batchId = null; - if (options.UseBeeApi) - { - batchId = await ethernaGatewayClient.BeeClient.BuyPostageBatchAsync(amount, batchDepth, label); - } - else - { - var batchReferenceId = await ethernaGatewayClient.BuyPostageBatchAsync(amount, batchDepth, label); - - // Wait until created batch is available. - ioService.Write("Waiting for batch created... (it may take a while)"); - - var batchStartWait = DateTime.UtcNow; - do - { - //timeout throw exception - if (DateTime.UtcNow - batchStartWait >= BatchCreationTimeout) - { - var ex = new InvalidOperationException("Batch not avaiable after timeout"); - ex.Data.Add("BatchReferenceId", batchReferenceId); - throw ex; - } - - try - { - batchId = await ethernaGatewayClient.TryGetNewPostageBatchIdFromPostageRefAsync(batchReferenceId); - } - catch (EthernaGatewayApiException) - { - //waiting for batchId available - await Task.Delay(BatchCheckTimeSpan); - } - } while (!batchId.HasValue); - - ioService.WriteLine(". Done"); - } - - await WaitForBatchUsableAsync(batchId.Value); - - return batchId.Value; - } - - public Task CreateTagAsync(PostageBatchId postageBatchId) => - ethernaGatewayClient.BeeClient.CreateTagAsync(postageBatchId); - - public Task FundResourceDownloadAsync(SwarmHash hash) => - ethernaGatewayClient.FundResourceDownloadAsync(hash); - - public Task FundResourcePinningAsync(SwarmHash hash) => - ethernaGatewayClient.FundResourcePinningAsync(hash); - - public async Task GetChainPriceAsync() - { - if (options.UseBeeApi) - return (await ethernaGatewayClient.BeeClient.GetChainStateAsync()).CurrentPrice; - return (await ethernaGatewayClient.GetChainStateAsync()).CurrentPrice; - } - - public Task GetChunkUploaderWebSocketAsync( - PostageBatchId batchId, - TagId? tagId = null, - CancellationToken cancellationToken = default) => - ethernaGatewayClient.BeeClient.GetChunkUploaderWebSocketAsync(batchId, tagId, cancellationToken); - - public Task GetPostageBatchInfoAsync(PostageBatchId batchId) - { - if (options.UseBeeApi) - { - return ethernaGatewayClient.BeeClient.GetPostageBatchAsync(batchId); - } - else - { - return ethernaGatewayClient.GetPostageBatchAsync(batchId); - } - } - - public async Task GetUsablePostageBatchIdAsync( - int requiredBatchDepth, - PostageBatchId? usePostageBatchId, - TimeSpan newPostageTtl, - bool newPostageAutoPurchase, - string? newPostageLabel) - { - if (usePostageBatchId is null) - { - //create a new postage batch - var chainPrice = await GetChainPriceAsync(); - var amount = PostageBatch.CalculateAmount(chainPrice, newPostageTtl); - var bzzPrice = PostageBatch.CalculatePrice(amount, requiredBatchDepth); - - ioService.WriteLine($"Required postage batch Depth: {requiredBatchDepth}, Amount: {amount.ToPlurString()}, BZZ price: {bzzPrice}"); - - if (!newPostageAutoPurchase) - { - bool validSelection = false; - - while (validSelection == false) - { - ioService.WriteLine($"Confirm the batch purchase? Y to confirm, N to deny [Y|n]"); - - switch (ioService.ReadKey()) - { - case { Key: ConsoleKey.Y }: - case { Key: ConsoleKey.Enter }: - validSelection = true; - break; - case { Key: ConsoleKey.N }: - throw new InvalidOperationException("Batch purchase denied"); - default: - ioService.WriteLine("Invalid selection"); - break; - } - } - } - - //create batch - var postageBatchId = await CreatePostageBatchAsync(amount, requiredBatchDepth, newPostageLabel); - - ioService.WriteLine($"Created postage batch: {postageBatchId}"); - - return postageBatchId; - } - else - { - //get info about existing postage batch - PostageBatch postageBatch; - try - { - postageBatch = await GetPostageBatchInfoAsync(usePostageBatchId.Value); - } - catch (EthernaGatewayApiException e) when (e.StatusCode == 404) - { - ioService.WriteErrorLine($"Unable to find postage batch \"{usePostageBatchId}\"."); - throw; - } - - //verify if it is usable - if (!postageBatch.IsUsable) - { - ioService.WriteErrorLine($"Postage batch \"{usePostageBatchId}\" is not usable."); - throw new InvalidOperationException(); - } - - Console.WriteLine("Attention! Provided postage batch will be used without requirements checks!"); - //See: https://etherna.atlassian.net/browse/ESG-269 - // // verify if it has available space - // if (gatewayService.CalculatePostageBatchByteSize(postageBatch) - - // gatewayService.CalculateRequiredPostageBatchSpace(contentByteSize) < 0) - // { - // IoService.WriteErrorLine($"Postage batch \"{Options.UsePostageBatchId}\" has not enough space."); - // throw new InvalidOperationException(); - // } - - return postageBatch.Id; - } - } - - public Task UploadFileAsync( - PostageBatchId batchId, - Stream content, - string? name, - string? contentType, - bool pinResource) => - ethernaGatewayClient.UploadFileAsync( - batchId, - content, - name: name, - contentType: contentType, - swarmDeferredUpload: true, - swarmPin: pinResource); - - public Task UploadDirectoryAsync( - PostageBatchId batchId, - string directoryPath, - bool pinResource) => - ethernaGatewayClient.UploadDirectoryAsync( - batchId, - directoryPath, - swarmDeferredUpload: true, - swarmPin: pinResource); - - // Helpers. - private async Task WaitForBatchUsableAsync(PostageBatchId batchId) - { - // Wait until created batch is usable. - ioService.Write("Waiting for batch being usable... (it may take a while)"); - - var batchStartWait = DateTime.UtcNow; - bool batchIsUsable; - do - { - //timeout throw exception - if (DateTime.UtcNow - batchStartWait >= BatchUsableTimeout) - { - var ex = new InvalidOperationException("Batch not usable after timeout"); - ex.Data.Add("BatchId", batchId); - throw ex; - } - - batchIsUsable = (await GetPostageBatchInfoAsync(batchId)).IsUsable; - - //waiting for batch usable - if (!batchIsUsable) - await Task.Delay(BatchCheckTimeSpan); - } while (!batchIsUsable); - - ioService.WriteLine(". Done"); - } - } -} \ No newline at end of file diff --git a/src/EthernaGatewayCli/Services/IGatewayService.cs b/src/EthernaGatewayCli/Services/IGatewayService.cs deleted file mode 100644 index e93cd13..0000000 --- a/src/EthernaGatewayCli/Services/IGatewayService.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2024-present Etherna SA -// This file is part of Etherna Gateway CLI. -// -// Etherna Gateway CLI is free software: you can redistribute it and/or modify it under the terms of the -// GNU Affero General Public License as published by the Free Software Foundation, -// either version 3 of the License, or (at your option) any later version. -// -// Etherna Gateway CLI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Etherna Gateway CLI. -// If not, see . - -using Etherna.BeeNet.Models; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Etherna.GatewayCli.Services -{ - public interface IGatewayService - { - Task CalculatePostageBatchDepthAsync(Stream fileStream, string fileContentType, string fileName); - - Task CalculatePostageBatchDepthAsync(byte[] fileData, string fileContentType, string fileName); - - Task CalculatePostageBatchDepthAsync(IEnumerable paths); - - Task CreatePostageBatchAsync(BzzBalance amount, int batchDepth, string? label); - - Task CreateTagAsync(PostageBatchId postageBatchId); - - Task FundResourceDownloadAsync(SwarmHash hash); - - Task FundResourcePinningAsync(SwarmHash hash); - - Task GetChainPriceAsync(); - - Task GetChunkUploaderWebSocketAsync( - PostageBatchId batchId, - TagId? tagId = null, - CancellationToken cancellationToken = default); - - Task GetPostageBatchInfoAsync(PostageBatchId batchId); - - Task GetUsablePostageBatchIdAsync( - int requiredBatchDepth, - PostageBatchId? usePostageBatchId, - TimeSpan newPostageTtl, - bool newPostageAutoPurchase, - string? newPostageLabel); - - Task UploadFileAsync( - PostageBatchId batchId, - Stream content, - string? name, - string? contentType, - bool pinResource); - - Task UploadDirectoryAsync( - PostageBatchId batchId, - string directoryPath, - bool pinResource); - } -} \ No newline at end of file diff --git a/src/EthernaGatewayCli/Services/Options/GatewayServiceOptions.cs b/src/EthernaGatewayCli/Services/IPostageBatchService.cs similarity index 62% rename from src/EthernaGatewayCli/Services/Options/GatewayServiceOptions.cs rename to src/EthernaGatewayCli/Services/IPostageBatchService.cs index c95b9f3..d78c703 100644 --- a/src/EthernaGatewayCli/Services/Options/GatewayServiceOptions.cs +++ b/src/EthernaGatewayCli/Services/IPostageBatchService.cs @@ -12,11 +12,21 @@ // You should have received a copy of the GNU Affero General Public License along with Etherna Gateway CLI. // If not, see . -namespace Etherna.GatewayCli.Services.Options +using Etherna.BeeNet.Models; +using System; +using System.Threading.Tasks; + +namespace Etherna.GatewayCli.Services { - public class GatewayServiceOptions + public interface IPostageBatchService { - // Properties. - public bool UseBeeApi { get; set; } + Task CalculatePostageBatchDepthAsync(string[] paths); + + Task GetUsablePostageBatchAsync( + int minBatchDepth, + TimeSpan minBatchTtl, + bool autoPurchaseNewBatch, + PostageBatchId? useBatchId, + string? newBatchLabel); } } \ No newline at end of file diff --git a/src/EthernaGatewayCli/Services/PostageBatchService.cs b/src/EthernaGatewayCli/Services/PostageBatchService.cs new file mode 100644 index 0000000..5b99cfa --- /dev/null +++ b/src/EthernaGatewayCli/Services/PostageBatchService.cs @@ -0,0 +1,160 @@ +// Copyright 2024-present Etherna SA +// This file is part of Etherna Gateway CLI. +// +// Etherna Gateway CLI is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Etherna Gateway CLI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with Etherna Gateway CLI. +// If not, see . + +using Etherna.BeeNet.Hashing.Postage; +using Etherna.BeeNet.Models; +using Etherna.BeeNet.Services; +using Etherna.CliHelper.Services; +using Etherna.Sdk.Gateway.GenClients; +using Etherna.Sdk.Users.Gateway.Services; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Etherna.GatewayCli.Services +{ + public class PostageBatchService( + IChunkService chunkService, + IFileService fileService, + IGatewayService gatewayService, + IIoService ioService) + : IPostageBatchService + { + public async Task CalculatePostageBatchDepthAsync(string[] paths) + { + ArgumentNullException.ThrowIfNull(paths, nameof(paths)); + if (paths.Length == 0) + throw new ArgumentOutOfRangeException(nameof(paths), "Empty file paths"); + + ioService.Write("Calculating required postage batch depth... "); + + var stampIssuer = new PostageStampIssuer(PostageBatch.MaxDepthInstance); + UploadEvaluationResult lastResult = null!; + foreach (var path in paths) + { + if (File.Exists(path)) //is a file + { + await using var fileStream = File.OpenRead(path); + var mimeType = fileService.GetMimeType(path); + var fileName = Path.GetFileName(path); + + lastResult = await chunkService.EvaluateSingleFileUploadAsync( + fileStream, + mimeType, + fileName, + postageStampIssuer: stampIssuer); + } + else if (Directory.Exists(path)) //is a directory + { + lastResult = await chunkService.EvaluateDirectoryUploadAsync( + path, + postageStampIssuer: stampIssuer); + } + else //invalid path + throw new InvalidOperationException($"Path {path} is not valid"); + } + + ioService.WriteLine("Done"); + + return lastResult.PostageStampIssuer.Buckets.RequiredPostageBatchDepth; + } + + public async Task GetUsablePostageBatchAsync( + int minBatchDepth, + TimeSpan minBatchTtl, + bool autoPurchaseNewBatch, + PostageBatchId? useBatchId, + string? newBatchLabel) + { + if (useBatchId is null) + { + //create a new postage batch + var chainPrice = await gatewayService.GetChainPriceAsync(); + var amount = PostageBatch.CalculateAmount(chainPrice, minBatchTtl); + var bzzPrice = PostageBatch.CalculatePrice(amount, minBatchDepth); + + ioService.WriteLine($"Required postage batch Depth: {minBatchDepth}, Amount: {amount.ToPlurString()}, BZZ price: {bzzPrice}"); + + if (!autoPurchaseNewBatch) + { + var validSelection = false; + while (validSelection == false) + { + ioService.WriteLine($"Confirm the batch purchase? Y to confirm, N to deny [Y|n]"); + + switch (ioService.ReadKey()) + { + case { Key: ConsoleKey.Y }: + case { Key: ConsoleKey.Enter }: + validSelection = true; + break; + case { Key: ConsoleKey.N }: + throw new InvalidOperationException("Batch purchase denied"); + default: + ioService.WriteLine("Invalid selection"); + break; + } + } + } + + //create batch + var batchId = await gatewayService.CreatePostageBatchAsync( + amount, + minBatchDepth, + newBatchLabel, + onWaitingBatchCreation: () => ioService.Write("Waiting for batch created... (it may take a while)"), + onBatchCreated: _ => ioService.WriteLine(". Done"), + onWaitingBatchUsable: () => ioService.Write("Waiting for batch being usable... (it may take a while)"), + onBatchUsable: () => ioService.WriteLine(". Done")); + + ioService.WriteLine($"Created postage batch: {batchId}"); + + return batchId; + } + else + { + //get info about existing postage batch + PostageBatch postageBatch; + try + { + postageBatch = await gatewayService.GetPostageBatchInfoAsync(useBatchId.Value); + } + catch (EthernaGatewayApiException e) when (e.StatusCode == 404) + { + ioService.WriteErrorLine($"Unable to find postage batch \"{useBatchId}\"."); + throw; + } + + //verify if it is usable + if (!postageBatch.IsUsable) + { + ioService.WriteErrorLine($"Postage batch \"{useBatchId}\" is not usable."); + throw new InvalidOperationException(); + } + + Console.WriteLine("Attention! Provided postage batch will be used without requirements checks!"); + //See: https://etherna.atlassian.net/browse/ESG-269 + // // verify if it has available space + // if (gatewayService.CalculatePostageBatchByteSize(postageBatch) - + // gatewayService.CalculateRequiredPostageBatchSpace(contentByteSize) < 0) + // { + // IoService.WriteErrorLine($"Postage batch \"{Options.UsePostageBatchId}\" has not enough space."); + // throw new InvalidOperationException(); + // } + + return postageBatch.Id; + } + } + } +} \ No newline at end of file