From 202b4cfb3a2df10351ffb874a2bc2ef3a32c4080 Mon Sep 17 00:00:00 2001
From: ImoutoChan <kirirrin@gmail.com>
Date: Sun, 31 Mar 2024 13:39:40 +0500
Subject: [PATCH] Add in-memory settings to enable/disable imouto pics upload

---
 CHANGELOG.md                                  |   5 +
 .../IImoutoPicsUploaderStateService.cs        |  10 +
 .../ImoutoPicsUploaderStateService.cs         |  20 +
 .../Services/ServiceLocator.cs                |   2 +-
 .../View/Flyouts/SettingsView.xaml            |   9 +
 .../ViewModel/MainWindowVM.cs                 |   1 +
 .../ViewModel/SettingsVM.cs                   |  34 ++
 .../EnableImoutoPicsUploaderCommand.cs        |  34 ++
 .../IsImoutoPicsUploaderEnabledQuery.cs       |  21 +
 .../Cqrs/OverseeCollectionCommand.cs          |   7 +-
 .../ServiceCollectionExtensions.cs            |   3 +
 .../Services/IImoutoPicsUploaderRepository.cs |  18 +
 .../ImoutoPicsUploaderState.cs                |  10 +
 .../CollectionFileSystemTests.cs              |  53 +++
 .../Fixtures/TestWebApplicationFactory.cs     |   8 +
 .../ImoutoPicsUploaderEnabledTests.cs         |  43 ++
 ...ImoutoRebirth.Room.IntegrationTests.csproj |   1 +
 .../WebApi/EndpointsMappings.cs               |  18 +
 .../WebApi/WebApiStartup.cs                   |   1 +
 .../Clients.cs                                | 407 +++++++++++++++++-
 20 files changed, 683 insertions(+), 22 deletions(-)
 create mode 100644 Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/IImoutoPicsUploaderStateService.cs
 create mode 100644 Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/ImoutoPicsUploaderStateService.cs
 create mode 100644 Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/EnableImoutoPicsUploaderCommand.cs
 create mode 100644 Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/IsImoutoPicsUploaderEnabledQuery.cs
 create mode 100644 Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Services/IImoutoPicsUploaderRepository.cs
 create mode 100644 Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Domain/ImoutoPicsUploaderState.cs
 create mode 100644 Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoPicsUploaderEnabledTests.cs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75cab577..8948485b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,15 @@
 
 ### Navigator
 * Fix webp previews in the list view
+* Add button to enable/disable imouto pics upload in Room
 
 ### Viewer
 * Add jfif to the list of supported image formats
 
+### Room
+* Add in-memory settings to enable/disable imouto pics upload and api for it
+* Add tests for the imouto pics upload settings
+
 # 4.24.0
 
 ### Navigator
diff --git a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/IImoutoPicsUploaderStateService.cs b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/IImoutoPicsUploaderStateService.cs
new file mode 100644
index 00000000..17115500
--- /dev/null
+++ b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/IImoutoPicsUploaderStateService.cs
@@ -0,0 +1,10 @@
+namespace ImoutoRebirth.Navigator.Services.Collections;
+
+public interface IImoutoPicsUploaderStateService
+{
+    Task EnableAsync();
+
+    Task DisableAsync();
+
+    Task<bool> IsEnabledAsync();
+}
diff --git a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/ImoutoPicsUploaderStateService.cs b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/ImoutoPicsUploaderStateService.cs
new file mode 100644
index 00000000..8c7eafab
--- /dev/null
+++ b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/Collections/ImoutoPicsUploaderStateService.cs
@@ -0,0 +1,20 @@
+using ImoutoRebirth.Room.WebApi.Client;
+
+namespace ImoutoRebirth.Navigator.Services.Collections;
+
+internal class ImoutoPicsUploaderStateService : IImoutoPicsUploaderStateService
+{
+    private readonly ImoutoPicsUploaderEnabledClient _imoutoPicsUploaderEnabledClient;
+
+    public ImoutoPicsUploaderStateService(ImoutoPicsUploaderEnabledClient imoutoPicsUploaderEnabledClient) 
+        => _imoutoPicsUploaderEnabledClient = imoutoPicsUploaderEnabledClient;
+
+    public async Task EnableAsync() 
+        => await _imoutoPicsUploaderEnabledClient.EnableImoutoPicsUploaderAsync();
+
+    public async Task DisableAsync() 
+        => await _imoutoPicsUploaderEnabledClient.DisableImoutoPicsUploaderAsync();
+
+    public async Task<bool> IsEnabledAsync() 
+        => await _imoutoPicsUploaderEnabledClient.IsImoutoPicsUploaderEnabledAsync();
+}
diff --git a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/ServiceLocator.cs b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/ServiceLocator.cs
index 7f4a8587..0c238513 100644
--- a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/ServiceLocator.cs
+++ b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/Services/ServiceLocator.cs
@@ -23,7 +23,7 @@ static ServiceLocator()
         sc.AddTransient<ICollectionService, CollectionService>();
         sc.AddTransient<IDestinationFolderService, DestinationFolderService>();
         sc.AddTransient<ISourceFolderService, SourceFolderService>();
-
+        sc.AddTransient<IImoutoPicsUploaderStateService, ImoutoPicsUploaderStateService>();
 
         sc.AddTransient<IFileService, FileService>();
         sc.AddTransient<IFileTagService, FileTagService>();
diff --git a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/View/Flyouts/SettingsView.xaml b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/View/Flyouts/SettingsView.xaml
index 2bd38bcd..1188562d 100644
--- a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/View/Flyouts/SettingsView.xaml
+++ b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/View/Flyouts/SettingsView.xaml
@@ -123,6 +123,15 @@
                                        Header="Auto shuffle on every load"
                                        IsOn="{Binding AutoShuffle}" />
 
+                <controls:ToggleSwitch Margin="0,25,0,0"
+                                       Header="Is ImoutoPics integration active"
+                                       IsOn="{Binding IsImoutoPicsUploaderEnabled}"
+                                       IsEnabled="False" />
+                
+                <Button Content="Toggle ImoutoPics"
+                        Command="{Binding ToggleImoutoPicsCommand}"
+                        Margin="0,25,0,0" />
+
                 <TextBlock Margin="0,25,0,0"
                            Text="Path Overrides" />
                 
diff --git a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/MainWindowVM.cs b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/MainWindowVM.cs
index 5bc35e50..f659f61b 100644
--- a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/MainWindowVM.cs
+++ b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/MainWindowVM.cs
@@ -133,6 +133,7 @@ private async Task InitializeAsync()
     {
         var topTagsUpdateTask = TagsEdit.UpdateUsersTopTags();
         await CollectionManager.ReloadCollectionsAsync();
+        await Settings.InitializeAsync();
         TagSearchVM.AddCollections(CollectionManager.Collections);
         await topTagsUpdateTask;
     }
diff --git a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/SettingsVM.cs b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/SettingsVM.cs
index 01d5bee9..39f45136 100644
--- a/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/SettingsVM.cs
+++ b/Source/ImoutoRebirth.Navigator/ImoutoRebirth.Navigator/ViewModel/SettingsVM.cs
@@ -6,6 +6,8 @@
 using ImoutoRebirth.Common;
 using ImoutoRebirth.Common.WPF;
 using ImoutoRebirth.Common.WPF.Commands;
+using ImoutoRebirth.Navigator.Services;
+using ImoutoRebirth.Navigator.Services.Collections;
 using MahApps.Metro.Theming;
 
 namespace ImoutoRebirth.Navigator.ViewModel;
@@ -15,7 +17,10 @@ internal class SettingsVM : VMBase
     private AccentColorMenuData _selectedAccentColor;
     private int _selectedTheme;
     private ICommand? _saveCommand;
+    private ICommand? _toggleImoutoPicsCommand;
     private string _pathOverrides;
+    private readonly IImoutoPicsUploaderStateService _imoutoPicsUploaderStateService;
+    private bool _isImoutoPicsUploaderEnabled;
 
     public SettingsVM()
     {
@@ -32,6 +37,8 @@ public SettingsVM()
 
         ShowPreviewOnSelect = Settings.Default.ActivatePreviewOnSelect;
         PathOverrides = Settings.Default.PathOverrides;
+
+        _imoutoPicsUploaderStateService = ServiceLocator.GetService<IImoutoPicsUploaderStateService>();
     }
 
     public bool ShowPreviewOnSelect
@@ -138,6 +145,12 @@ public string LilinHost
         set => Settings.Default.LilinHost = value;
     }
 
+    public bool IsImoutoPicsUploaderEnabled
+    {
+        get => _isImoutoPicsUploaderEnabled;
+        set => OnPropertyChanged(ref _isImoutoPicsUploaderEnabled, value, () => IsImoutoPicsUploaderEnabled);
+    }
+
     public string RoomHost
     {
         get => Settings.Default.RoomHost;
@@ -146,6 +159,8 @@ public string RoomHost
 
     public ICommand SaveCommand => _saveCommand ??= new RelayCommand(_ => Save());
 
+    public ICommand ToggleImoutoPicsCommand => _toggleImoutoPicsCommand ??= new AsyncCommand(() => ToggleImoutoPics());
+
     private static void Save() => Settings.Default.Save();
 
     public event EventHandler? ShowPreviewOnSelectChanged;
@@ -155,6 +170,25 @@ private void OnShowPreviewOnSelectChanged()
         var handler = ShowPreviewOnSelectChanged;
         handler?.Invoke(this, EventArgs.Empty);
     }
+
+    public async Task InitializeAsync()
+    {
+        IsImoutoPicsUploaderEnabled = await _imoutoPicsUploaderStateService.IsEnabledAsync();
+    }
+    
+    private async Task ToggleImoutoPics()
+    {
+        if (IsImoutoPicsUploaderEnabled)
+        {
+            await _imoutoPicsUploaderStateService.DisableAsync();
+        }
+        else
+        {
+            await _imoutoPicsUploaderStateService.EnableAsync();
+        }
+        
+        IsImoutoPicsUploaderEnabled = await _imoutoPicsUploaderStateService.IsEnabledAsync();
+    }
 }
 
 public class AccentColorMenuData
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/EnableImoutoPicsUploaderCommand.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/EnableImoutoPicsUploaderCommand.cs
new file mode 100644
index 00000000..a5a2c1db
--- /dev/null
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/EnableImoutoPicsUploaderCommand.cs
@@ -0,0 +1,34 @@
+using ImoutoRebirth.Common.Cqrs.Abstract;
+using ImoutoRebirth.Room.Application.Services;
+
+namespace ImoutoRebirth.Room.Application.Cqrs.ImoutoPicsUploadStateSlice;
+
+public record EnableImoutoPicsUploaderCommand : ICommand;
+
+public record DisableImoutoPicsUploaderCommand : ICommand;
+
+internal class EnableImoutoPicsUploaderCommandHandler 
+    : ICommandHandler<EnableImoutoPicsUploaderCommand>
+    , ICommandHandler<DisableImoutoPicsUploaderCommand>
+{
+    private readonly IImoutoPicsUploaderRepository _imoutoPicsUploaderRepository;
+
+    public EnableImoutoPicsUploaderCommandHandler(IImoutoPicsUploaderRepository imoutoPicsUploaderRepository) 
+        => _imoutoPicsUploaderRepository = imoutoPicsUploaderRepository;
+
+    public Task Handle(EnableImoutoPicsUploaderCommand _, CancellationToken ct)
+    {
+        var state = _imoutoPicsUploaderRepository.Get();
+        state.Enable();
+        
+        return Task.CompletedTask;
+    }
+    
+    public Task Handle(DisableImoutoPicsUploaderCommand _, CancellationToken ct)
+    {
+        var state = _imoutoPicsUploaderRepository.Get();
+        state.Disable();
+        
+        return Task.CompletedTask;
+    }
+}
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/IsImoutoPicsUploaderEnabledQuery.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/IsImoutoPicsUploaderEnabledQuery.cs
new file mode 100644
index 00000000..8c013a40
--- /dev/null
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/ImoutoPicsUploadStateSlice/IsImoutoPicsUploaderEnabledQuery.cs
@@ -0,0 +1,21 @@
+using ImoutoRebirth.Common.Cqrs.Abstract;
+using ImoutoRebirth.Room.Application.Services;
+
+namespace ImoutoRebirth.Room.Application.Cqrs.ImoutoPicsUploadStateSlice;
+
+public record IsImoutoPicsUploaderEnabledQuery : IQuery<bool>;
+
+internal class IsImoutoPicsUploaderEnabledQueryHandler 
+    : IQueryHandler<IsImoutoPicsUploaderEnabledQuery, bool>
+{
+    private readonly IImoutoPicsUploaderRepository _imoutoPicsUploaderRepository;
+
+    public IsImoutoPicsUploaderEnabledQueryHandler(IImoutoPicsUploaderRepository imoutoPicsUploaderRepository) 
+        => _imoutoPicsUploaderRepository = imoutoPicsUploaderRepository;
+
+    public Task<bool> Handle(IsImoutoPicsUploaderEnabledQuery _, CancellationToken ct)
+    {
+        var state = _imoutoPicsUploaderRepository.Get();
+        return Task.FromResult(state.IsEnabled);
+    }
+}
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/OverseeCollectionCommand.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/OverseeCollectionCommand.cs
index ab60228a..2b496ed3 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/OverseeCollectionCommand.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Cqrs/OverseeCollectionCommand.cs
@@ -1,6 +1,7 @@
 using System.Runtime.CompilerServices;
 using ImoutoRebirth.Common.Cqrs.Abstract;
 using ImoutoRebirth.Common.Domain;
+using ImoutoRebirth.Room.Application.Cqrs.ImoutoPicsUploadStateSlice;
 using ImoutoRebirth.Room.Application.Services;
 using ImoutoRebirth.Room.Domain;
 using ImoutoRebirth.Room.Domain.CollectionAggregate;
@@ -73,7 +74,11 @@ private async Task SaveAndReport(Guid collectionId, SystemFileMoved moved)
 
         await _remoteCommandService.SaveTags(newId, moved.SourceTags);
         await _remoteCommandService.UpdateMetadataRequest(newId, moved.SystemFile.Md5);
-        await _imoutoPicsUploader.UploadFile(moved.MovedFileInfo.FullName);
+        
+        var isUploadEnabled = await _mediator.Send(new IsImoutoPicsUploaderEnabledQuery());
+        
+        if (isUploadEnabled)
+            await _imoutoPicsUploader.UploadFile(moved.MovedFileInfo.FullName);
     }
 
     private SystemFileMoved MoveFile(Collection collection, SystemFilePreparedToMove preparedToMove)
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/ServiceCollectionExtensions.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/ServiceCollectionExtensions.cs
index 91a85c1e..79a78090 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/ServiceCollectionExtensions.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/ServiceCollectionExtensions.cs
@@ -1,6 +1,7 @@
 using System.Reflection;
 using ImoutoRebirth.Common.Cqrs;
 using ImoutoRebirth.Room.Application.Cqrs;
+using ImoutoRebirth.Room.Application.Services;
 using Microsoft.Extensions.DependencyInjection;
 
 namespace ImoutoRebirth.Room.Application;
@@ -18,6 +19,8 @@ public static IServiceCollection AddRoomApplication(
         });
         services.AddLoggingBehavior();
         services.AddTransactionBehavior();
+        
+        services.AddSingleton<IImoutoPicsUploaderRepository, ImoutoPicsUploaderRepository>();
 
         return services;
     }
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Services/IImoutoPicsUploaderRepository.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Services/IImoutoPicsUploaderRepository.cs
new file mode 100644
index 00000000..56f46549
--- /dev/null
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Application/Services/IImoutoPicsUploaderRepository.cs
@@ -0,0 +1,18 @@
+using ImoutoRebirth.Room.Domain;
+
+namespace ImoutoRebirth.Room.Application.Services;
+
+public interface IImoutoPicsUploaderRepository
+{
+    ImoutoPicsUploaderState Get();
+}
+
+/// <summary>
+/// Should be registered as a singleton.
+/// </summary>
+public class ImoutoPicsUploaderRepository : IImoutoPicsUploaderRepository
+{
+    private readonly ImoutoPicsUploaderState _state = new();
+    
+    public ImoutoPicsUploaderState Get() => _state;
+}
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Domain/ImoutoPicsUploaderState.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Domain/ImoutoPicsUploaderState.cs
new file mode 100644
index 00000000..d7e30ccb
--- /dev/null
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.Domain/ImoutoPicsUploaderState.cs
@@ -0,0 +1,10 @@
+namespace ImoutoRebirth.Room.Domain;
+
+public class ImoutoPicsUploaderState
+{
+    public bool IsEnabled { get; private set; } = true;
+
+    public void Enable() => IsEnabled = true;
+    
+    public void Disable() => IsEnabled = false;
+}
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/CollectionFileSystemTests.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/CollectionFileSystemTests.cs
index 5b508f54..3a111495 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/CollectionFileSystemTests.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/CollectionFileSystemTests.cs
@@ -11,6 +11,7 @@
 using MediatR;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
+using Moq;
 using Xunit;
 
 namespace ImoutoRebirth.Room.IntegrationTests;
@@ -807,6 +808,58 @@ public async Task MultipleFileIsMovedToDestinationWithSameName()
             .Should().Be(Path.Combine(destFolderPath, "file1-5f30f9953332c230d11e3f26db5ae9a0.jpg"));
     }
 
+    [Fact]
+    public async Task ImoutoPicsUploadCalledWhenImoutoPicsUploaderEnabled()
+    {
+        // arrange
+        var (_, sourceFolderPath, destFolderPath) = await CreateDefaultCollection(
+            sourceShouldCheckFormat:              false,
+            sourceShouldCheckHashFromName:        false ,
+            sourceShouldCreateTagsFromSubfolders: false,
+            sourceShouldAddTagFromFilename:       false,
+            sourceSupportedExtensions:            new []{ "jpg" },
+            destShouldCreateSubfoldersByHash:     false,
+            destShouldRenameByHash:               false);
+        
+        var testFile = new FileInfo(Path.Combine(_webApp.TestsLocation, "Resources", "file1-5f30f9953332c230d11e3f26db5ae9a0.jpg"));
+        
+        await _webApp.Client.PostAsync("/imouto-pics-uploader-enabled", null);
+        
+        // act
+        testFile.CopyTo(Path.Combine(sourceFolderPath, testFile.Name));
+        await _mediator.Send(new OverseeCommand());
+        
+        // assert
+        var file = Path.Combine(destFolderPath, "file1-5f30f9953332c230d11e3f26db5ae9a0.jpg");
+        _webApp.ImoutoPicsUploaderMock.Verify(x => x.UploadFile(file), Times.Once);
+    }
+
+    [Fact()]
+    public async Task ImoutoPicsUploadShouldNotBeCalledWhenImoutoPicsUploaderDisabled()
+    {
+        // arrange
+        var (_, sourceFolderPath, destFolderPath) = await CreateDefaultCollection(
+            sourceShouldCheckFormat:              false,
+            sourceShouldCheckHashFromName:        false ,
+            sourceShouldCreateTagsFromSubfolders: false,
+            sourceShouldAddTagFromFilename:       false,
+            sourceSupportedExtensions:            new []{ "jpg" },
+            destShouldCreateSubfoldersByHash:     false,
+            destShouldRenameByHash:               false);
+        
+        var testFile = new FileInfo(Path.Combine(_webApp.TestsLocation, "Resources", "file1-5f30f9953332c230d11e3f26db5ae9a0.jpg"));
+        
+        await _webApp.Client.DeleteAsync("/imouto-pics-uploader-enabled");
+        
+        // act
+        testFile.CopyTo(Path.Combine(sourceFolderPath, testFile.Name));
+        await _mediator.Send(new OverseeCommand());
+        
+        // assert
+        var file = Path.Combine(destFolderPath, "file1-5f30f9953332c230d11e3f26db5ae9a0.jpg");
+        _webApp.ImoutoPicsUploaderMock.Verify(x => x.UploadFile(file), Times.Never);
+    }
+
     private async Task<CreatedCollection> CreateDefaultCollection(
         bool sourceShouldCheckFormat = false,
         bool sourceShouldCheckHashFromName = false,
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/Fixtures/TestWebApplicationFactory.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/Fixtures/TestWebApplicationFactory.cs
index d369ba97..93278f81 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/Fixtures/TestWebApplicationFactory.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/Fixtures/TestWebApplicationFactory.cs
@@ -1,11 +1,13 @@
 using System.Reflection;
 using ImoutoRebirth.Common.MassTransit;
+using ImoutoRebirth.Room.Application.Services;
 using ImoutoRebirth.Room.Database;
 using ImoutoRebirth.Room.Infrastructure;
 using Microsoft.AspNetCore.Mvc.Testing;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
+using Moq;
 using Npgsql;
 using Xunit;
 
@@ -33,6 +35,8 @@ public string TestsTempLocation
     public string TestsLocation 
         => new FileInfo(Assembly.GetExecutingAssembly().Location).Directory!.FullName;
 
+    public Mock<IImoutoPicsUploader> ImoutoPicsUploaderMock { get; } = new();
+    
     protected override IHost CreateHost(IHostBuilder builder)
     {
         builder.ConfigureServices(services =>
@@ -47,6 +51,9 @@ protected override IHost CreateHost(IHostBuilder builder)
                 .Union(services.Where(
                     d => d.ServiceType == typeof(IHostedService)
                          && d.ImplementationType?.Name == "QuartzHostedService"))
+                
+                // replace IImoutoPicsUploader with mock
+                .Union(services.Where(x => x.ServiceType == typeof(IImoutoPicsUploader)))
                 .ToList();
 
             foreach (var descriptor in descriptors)
@@ -54,6 +61,7 @@ protected override IHost CreateHost(IHostBuilder builder)
 
             services.AddDbContext<RoomDbContext>(x => x.UseNpgsql(ConnectionString, y => y.UseNodaTime()));
             services.AddMassTransitTestHarness(с => с.AddRoomMassTransitSetup());
+            services.AddTransient<IImoutoPicsUploader>(x => ImoutoPicsUploaderMock.Object);
         });
 
         var host = base.CreateHost(builder);
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoPicsUploaderEnabledTests.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoPicsUploaderEnabledTests.cs
new file mode 100644
index 00000000..00b17dbd
--- /dev/null
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoPicsUploaderEnabledTests.cs
@@ -0,0 +1,43 @@
+using Xunit;
+using System.Net.Http.Json;
+using ImoutoRebirth.Room.IntegrationTests.Fixtures;
+
+namespace ImoutoRebirth.Room.IntegrationTests
+{
+    [Collection("WebApplication")]
+    public class ImoutoPicsUploaderEnabledTests
+    {
+        private readonly TestWebApplicationFactory<Program> _webApp;
+
+        public ImoutoPicsUploaderEnabledTests(TestWebApplicationFactory<Program> webApp) => _webApp = webApp;
+
+        [Fact]
+        public async Task ImoutoPicsUploaderEnabledByDefault()
+        {
+            var response = await _webApp.Client.GetFromJsonAsync<bool>("/imouto-pics-uploader-enabled");
+            Assert.True(response);
+        }
+
+        [Fact]
+        public async Task DisableImoutoPicsUploader()
+        {
+            var response = await _webApp.Client.DeleteAsync("/imouto-pics-uploader-enabled");
+            response.EnsureSuccessStatusCode();
+
+            var isEnabled = await _webApp.Client.GetFromJsonAsync<bool>("/imouto-pics-uploader-enabled");
+            Assert.False(isEnabled);
+        }
+
+        [Fact]
+        public async Task EnableImoutoPicsUploader()
+        {
+            await _webApp.Client.PostAsync("/imouto-pics-uploader-enabled/disable", null);
+
+            var response = await _webApp.Client.PostAsync("/imouto-pics-uploader-enabled", null);
+            response.EnsureSuccessStatusCode();
+
+            var isEnabled = await _webApp.Client.GetFromJsonAsync<bool>("/imouto-pics-uploader-enabled");
+            Assert.True(isEnabled);
+        }
+    }
+}
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoRebirth.Room.IntegrationTests.csproj b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoRebirth.Room.IntegrationTests.csproj
index 8ad62111..7c55a7d9 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoRebirth.Room.IntegrationTests.csproj
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.IntegrationTests/ImoutoRebirth.Room.IntegrationTests.csproj
@@ -18,6 +18,7 @@
         <PackageReference Include="FluentAssertions" Version="6.12.0" />
         <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
+        <PackageReference Include="Moq" Version="4.20.70" />
         <PackageReference Include="xunit" Version="2.7.0" />
         <PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
             <PrivateAssets>all</PrivateAssets>
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/EndpointsMappings.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/EndpointsMappings.cs
index efd54f17..dc3e674e 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/EndpointsMappings.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/EndpointsMappings.cs
@@ -3,6 +3,7 @@
 using ImoutoRebirth.Room.Application.Cqrs.CollectionFileSlice;
 using ImoutoRebirth.Room.Application.Cqrs.CollectionSlice;
 using ImoutoRebirth.Room.Application.Cqrs.FoldersSlice;
+using ImoutoRebirth.Room.Application.Cqrs.ImoutoPicsUploadStateSlice;
 using MediatR;
 using Microsoft.AspNetCore.Builder;
 
@@ -103,4 +104,21 @@ public static void MapSourceFoldersEndpoints(this WebApplication app)
                 => mediator.Send(new DeleteSourceFolderCommand(collectionId, sourceFolderId), ct))
             .WithName("DeleteSourceFolder");
     }
+
+    public static void MapImoutoPicsUploaderEnabled(this WebApplication app)
+    {
+        var sourceFolders = app.MapGroup("/imouto-pics-uploader-enabled");
+
+        sourceFolders.MapGet("", (IMediator mediator, CancellationToken ct) 
+                => mediator.Send(new IsImoutoPicsUploaderEnabledQuery(), ct))
+            .WithName("IsImoutoPicsUploaderEnabled");
+        
+        sourceFolders.MapPost("", (IMediator mediator, CancellationToken ct)
+                => mediator.Send(new EnableImoutoPicsUploaderCommand(), ct))
+            .WithName("EnableImoutoPicsUploader");
+
+        sourceFolders.MapDelete("", (IMediator mediator, CancellationToken ct)
+                => mediator.Send(new DisableImoutoPicsUploaderCommand(), ct))
+            .WithName("DisableImoutoPicsUploader");
+    }
 }
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/WebApiStartup.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/WebApiStartup.cs
index 913c79c5..f950f16c 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/WebApiStartup.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.UI/WebApi/WebApiStartup.cs
@@ -46,6 +46,7 @@ public static WebApplication MapWebEndpoints(this WebApplication app)
         app.MapCollectionFilesEndpoints();
         app.MapDestinationFoldersEndpoints();
         app.MapSourceFoldersEndpoints();
+        app.MapImoutoPicsUploaderEnabled();
         
         return app;
     }
diff --git a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.WebApi.Client/Clients.cs b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.WebApi.Client/Clients.cs
index 94a6586c..e4648fdd 100644
--- a/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.WebApi.Client/Clients.cs
+++ b/Source/ImoutoRebirth.Room/ImoutoRebirth.Room.WebApi.Client/Clients.cs
@@ -1,6 +1,6 @@
 //----------------------
 // <auto-generated>
-//     Generated using the NSwag toolchain v14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
+//     Generated using the NSwag toolchain v14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
 // </auto-generated>
 //----------------------
 
@@ -16,18 +16,22 @@
 #pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant"
 #pragma warning disable 8603 // Disable "CS8603 Possible null reference return"
 #pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter"
+#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type"
+#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes).
 
 namespace ImoutoRebirth.Room.WebApi.Client
 {
     using System = global::System;
 
-    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class CollectionFilesClient 
     {
         private System.Net.Http.HttpClient _httpClient;
         private static System.Lazy<System.Text.Json.JsonSerializerOptions> _settings = new System.Lazy<System.Text.Json.JsonSerializerOptions>(CreateSerializerSettings, true);
 
+    #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
         public CollectionFilesClient(System.Net.Http.HttpClient httpClient)
+    #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
         {
             _httpClient = httpClient;
         }
@@ -645,13 +649,15 @@ private string ConvertToString(object? value, System.Globalization.CultureInfo c
         }
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class CollectionsClient 
     {
         private System.Net.Http.HttpClient _httpClient;
         private static System.Lazy<System.Text.Json.JsonSerializerOptions> _settings = new System.Lazy<System.Text.Json.JsonSerializerOptions>(CreateSerializerSettings, true);
 
+    #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
         public CollectionsClient(System.Net.Http.HttpClient httpClient)
+    #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
         {
             _httpClient = httpClient;
         }
@@ -866,9 +872,9 @@ public virtual async System.Threading.Tasks.Task RenameCollectionAsync(System.Gu
                     // Operation Path: "collections/{collectionId}"
                     urlBuilder_.Append("collections/");
                     urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(collectionId, System.Globalization.CultureInfo.InvariantCulture)));
-            urlBuilder_.Append('?');
-            urlBuilder_.Append(System.Uri.EscapeDataString("newName")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(newName, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
-            urlBuilder_.Length--;
+                    urlBuilder_.Append('?');
+                    urlBuilder_.Append(System.Uri.EscapeDataString("newName")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(newName, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
+                    urlBuilder_.Length--;
 
                     PrepareRequest(client_, request_, urlBuilder_);
 
@@ -1675,7 +1681,367 @@ private string ConvertToString(object? value, System.Globalization.CultureInfo c
         }
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    public partial class ImoutoPicsUploaderEnabledClient 
+    {
+        private System.Net.Http.HttpClient _httpClient;
+        private static System.Lazy<System.Text.Json.JsonSerializerOptions> _settings = new System.Lazy<System.Text.Json.JsonSerializerOptions>(CreateSerializerSettings, true);
+
+    #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+        public ImoutoPicsUploaderEnabledClient(System.Net.Http.HttpClient httpClient)
+    #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+        {
+            _httpClient = httpClient;
+        }
+
+        private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings()
+        {
+            var settings = new System.Text.Json.JsonSerializerOptions();
+            UpdateJsonSerializerSettings(settings);
+            return settings;
+        }
+
+        protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _settings.Value; } }
+
+        static partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings);
+
+        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
+        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
+        partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
+
+        /// <returns>Success</returns>
+        /// <exception cref="WebApiException">A server side error occurred.</exception>
+        public virtual System.Threading.Tasks.Task<bool> IsImoutoPicsUploaderEnabledAsync()
+        {
+            return IsImoutoPicsUploaderEnabledAsync(System.Threading.CancellationToken.None);
+        }
+
+        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+        /// <returns>Success</returns>
+        /// <exception cref="WebApiException">A server side error occurred.</exception>
+        public virtual async System.Threading.Tasks.Task<bool> IsImoutoPicsUploaderEnabledAsync(System.Threading.CancellationToken cancellationToken)
+        {
+            var client_ = _httpClient;
+            var disposeClient_ = false;
+            try
+            {
+                using (var request_ = new System.Net.Http.HttpRequestMessage())
+                {
+                    request_.Method = new System.Net.Http.HttpMethod("GET");
+                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+                    var urlBuilder_ = new System.Text.StringBuilder();
+                
+                    // Operation Path: "imouto-pics-uploader-enabled"
+                    urlBuilder_.Append("imouto-pics-uploader-enabled");
+
+                    PrepareRequest(client_, request_, urlBuilder_);
+
+                    var url_ = urlBuilder_.ToString();
+                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+                    PrepareRequest(client_, request_, url_);
+
+                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+                    var disposeResponse_ = true;
+                    try
+                    {
+                        var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>();
+                        foreach (var item_ in response_.Headers)
+                            headers_[item_.Key] = item_.Value;
+                        if (response_.Content != null && response_.Content.Headers != null)
+                        {
+                            foreach (var item_ in response_.Content.Headers)
+                                headers_[item_.Key] = item_.Value;
+                        }
+
+                        ProcessResponse(client_, response_);
+
+                        var status_ = (int)response_.StatusCode;
+                        if (status_ == 200)
+                        {
+                            var objectResponse_ = await ReadObjectResponseAsync<bool>(response_, headers_, cancellationToken).ConfigureAwait(false);
+                            if (objectResponse_.Object == null)
+                            {
+                                throw new WebApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+                            }
+                            return objectResponse_.Object;
+                        }
+                        else
+                        {
+                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+                            throw new WebApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+                        }
+                    }
+                    finally
+                    {
+                        if (disposeResponse_)
+                            response_.Dispose();
+                    }
+                }
+            }
+            finally
+            {
+                if (disposeClient_)
+                    client_.Dispose();
+            }
+        }
+
+        /// <returns>Success</returns>
+        /// <exception cref="WebApiException">A server side error occurred.</exception>
+        public virtual System.Threading.Tasks.Task EnableImoutoPicsUploaderAsync()
+        {
+            return EnableImoutoPicsUploaderAsync(System.Threading.CancellationToken.None);
+        }
+
+        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+        /// <returns>Success</returns>
+        /// <exception cref="WebApiException">A server side error occurred.</exception>
+        public virtual async System.Threading.Tasks.Task EnableImoutoPicsUploaderAsync(System.Threading.CancellationToken cancellationToken)
+        {
+            var client_ = _httpClient;
+            var disposeClient_ = false;
+            try
+            {
+                using (var request_ = new System.Net.Http.HttpRequestMessage())
+                {
+                    request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
+                    request_.Method = new System.Net.Http.HttpMethod("POST");
+
+                    var urlBuilder_ = new System.Text.StringBuilder();
+                
+                    // Operation Path: "imouto-pics-uploader-enabled"
+                    urlBuilder_.Append("imouto-pics-uploader-enabled");
+
+                    PrepareRequest(client_, request_, urlBuilder_);
+
+                    var url_ = urlBuilder_.ToString();
+                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+                    PrepareRequest(client_, request_, url_);
+
+                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+                    var disposeResponse_ = true;
+                    try
+                    {
+                        var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>();
+                        foreach (var item_ in response_.Headers)
+                            headers_[item_.Key] = item_.Value;
+                        if (response_.Content != null && response_.Content.Headers != null)
+                        {
+                            foreach (var item_ in response_.Content.Headers)
+                                headers_[item_.Key] = item_.Value;
+                        }
+
+                        ProcessResponse(client_, response_);
+
+                        var status_ = (int)response_.StatusCode;
+                        if (status_ == 200)
+                        {
+                            return;
+                        }
+                        else
+                        {
+                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+                            throw new WebApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+                        }
+                    }
+                    finally
+                    {
+                        if (disposeResponse_)
+                            response_.Dispose();
+                    }
+                }
+            }
+            finally
+            {
+                if (disposeClient_)
+                    client_.Dispose();
+            }
+        }
+
+        /// <returns>Success</returns>
+        /// <exception cref="WebApiException">A server side error occurred.</exception>
+        public virtual System.Threading.Tasks.Task DisableImoutoPicsUploaderAsync()
+        {
+            return DisableImoutoPicsUploaderAsync(System.Threading.CancellationToken.None);
+        }
+
+        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+        /// <returns>Success</returns>
+        /// <exception cref="WebApiException">A server side error occurred.</exception>
+        public virtual async System.Threading.Tasks.Task DisableImoutoPicsUploaderAsync(System.Threading.CancellationToken cancellationToken)
+        {
+            var client_ = _httpClient;
+            var disposeClient_ = false;
+            try
+            {
+                using (var request_ = new System.Net.Http.HttpRequestMessage())
+                {
+                    request_.Method = new System.Net.Http.HttpMethod("DELETE");
+
+                    var urlBuilder_ = new System.Text.StringBuilder();
+                
+                    // Operation Path: "imouto-pics-uploader-enabled"
+                    urlBuilder_.Append("imouto-pics-uploader-enabled");
+
+                    PrepareRequest(client_, request_, urlBuilder_);
+
+                    var url_ = urlBuilder_.ToString();
+                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+                    PrepareRequest(client_, request_, url_);
+
+                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+                    var disposeResponse_ = true;
+                    try
+                    {
+                        var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>();
+                        foreach (var item_ in response_.Headers)
+                            headers_[item_.Key] = item_.Value;
+                        if (response_.Content != null && response_.Content.Headers != null)
+                        {
+                            foreach (var item_ in response_.Content.Headers)
+                                headers_[item_.Key] = item_.Value;
+                        }
+
+                        ProcessResponse(client_, response_);
+
+                        var status_ = (int)response_.StatusCode;
+                        if (status_ == 200)
+                        {
+                            return;
+                        }
+                        else
+                        {
+                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+                            throw new WebApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+                        }
+                    }
+                    finally
+                    {
+                        if (disposeResponse_)
+                            response_.Dispose();
+                    }
+                }
+            }
+            finally
+            {
+                if (disposeClient_)
+                    client_.Dispose();
+            }
+        }
+
+        protected struct ObjectResponseResult<T>
+        {
+            public ObjectResponseResult(T responseObject, string responseText)
+            {
+                this.Object = responseObject;
+                this.Text = responseText;
+            }
+
+            public T Object { get; }
+
+            public string Text { get; }
+        }
+
+        public bool ReadResponseAsString { get; set; }
+
+        protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
+        {
+            if (response == null || response.Content == null)
+            {
+                return new ObjectResponseResult<T>(default(T)!, string.Empty);
+            }
+
+            if (ReadResponseAsString)
+            {
+                var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+                try
+                {
+                    var typedBody = System.Text.Json.JsonSerializer.Deserialize<T>(responseText, JsonSerializerSettings);
+                    return new ObjectResponseResult<T>(typedBody!, responseText);
+                }
+                catch (System.Text.Json.JsonException exception)
+                {
+                    var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
+                    throw new WebApiException(message, (int)response.StatusCode, responseText, headers, exception);
+                }
+            }
+            else
+            {
+                try
+                {
+                    using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+                    {
+                        var typedBody = await System.Text.Json.JsonSerializer.DeserializeAsync<T>(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false);
+                        return new ObjectResponseResult<T>(typedBody!, string.Empty);
+                    }
+                }
+                catch (System.Text.Json.JsonException exception)
+                {
+                    var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
+                    throw new WebApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
+                }
+            }
+        }
+
+        private string ConvertToString(object? value, System.Globalization.CultureInfo cultureInfo)
+        {
+            if (value == null)
+            {
+                return "";
+            }
+
+            if (value is System.Enum)
+            {
+                var name = System.Enum.GetName(value.GetType(), value);
+                if (name != null)
+                {
+                    var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
+                    if (field != null)
+                    {
+                        var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) 
+                            as System.Runtime.Serialization.EnumMemberAttribute;
+                        if (attribute != null)
+                        {
+                            return attribute.Value != null ? attribute.Value : name;
+                        }
+                    }
+
+                    var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
+                    return converted == null ? string.Empty : converted;
+                }
+            }
+            else if (value is bool) 
+            {
+                return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
+            }
+            else if (value is byte[])
+            {
+                return System.Convert.ToBase64String((byte[]) value);
+            }
+            else if (value is string[])
+            {
+                return string.Join(",", (string[])value);
+            }
+            else if (value.GetType().IsArray)
+            {
+                var valueArray = (System.Array)value;
+                var valueTextArray = new string[valueArray.Length];
+                for (var i = 0; i < valueArray.Length; i++)
+                {
+                    valueTextArray[i] = ConvertToString(valueArray.GetValue(i), cultureInfo);
+                }
+                return string.Join(",", valueTextArray);
+            }
+
+            var result = System.Convert.ToString(value, cultureInfo);
+            return result == null ? "" : result;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class AddSourceFolderCommand
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1722,7 +2088,7 @@ public AddSourceFolderCommand(System.Guid @collectionId, string? @path, bool @sh
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class CollectionFile
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1764,7 +2130,7 @@ public CollectionFile(System.Guid @collectionId, System.Guid @id, string? @md5,
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class CollectionFilesQuery
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1806,7 +2172,7 @@ public CollectionFilesQuery(System.Collections.Generic.IReadOnlyCollection<Syste
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class CollectionResponse
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1828,7 +2194,7 @@ public CollectionResponse(System.Guid @id, string? @name)
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class CreateCollectionCommand
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1845,7 +2211,7 @@ public CreateCollectionCommand(string? @name)
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class DestinationFolderInfo
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1897,7 +2263,7 @@ public DestinationFolderInfo(System.Guid @collectionId, string? @formatErrorSubf
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class DestinationFolderInfoOptionalResponse
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1919,7 +2285,7 @@ public DestinationFolderInfoOptionalResponse(bool @hasValue, DestinationFolderIn
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class FilterCollectionFileHashesQuery
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1936,7 +2302,7 @@ public FilterCollectionFileHashesQuery(System.Collections.Generic.IReadOnlyColle
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class SetDestinationFolderCommand
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -1983,7 +2349,7 @@ public SetDestinationFolderCommand(System.Guid @collectionId, string? @formatErr
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class SourceFolderInfo
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -2035,7 +2401,7 @@ public SourceFolderInfo(System.Guid @collectionId, System.Guid @id, string? @pat
 
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class UpdateSourceFolderCommand
     {
         [System.Text.Json.Serialization.JsonConstructor]
@@ -2089,7 +2455,7 @@ public UpdateSourceFolderCommand(System.Guid @collectionId, string? @path, bool
 
 
 
-    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class WebApiException : System.Exception
     {
         public int StatusCode { get; private set; }
@@ -2112,7 +2478,7 @@ public override string ToString()
         }
     }
 
-    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
+    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
     public partial class WebApiException<TResult> : WebApiException
     {
         public TResult Result { get; private set; }
@@ -2135,4 +2501,5 @@ public WebApiException(string message, int statusCode, string? response, System.
 #pragma warning restore 8073
 #pragma warning restore 3016
 #pragma warning restore 8603
-#pragma warning restore 8604
\ No newline at end of file
+#pragma warning restore 8604
+#pragma warning restore 8625
\ No newline at end of file