diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index a146402ac..7da4b0661 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -33,4 +33,5 @@ + diff --git a/BTCPayServer.Tests/StorageTests.cs b/BTCPayServer.Tests/StorageTests.cs new file mode 100644 index 000000000..c20647228 --- /dev/null +++ b/BTCPayServer.Tests/StorageTests.cs @@ -0,0 +1,247 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BTCPayServer.Controllers; +using BTCPayServer.Models; +using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using BTCPayServer.Storage.ViewModels; +using BTCPayServer.Tests.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests +{ + public class StorageTests + { + public StorageTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact] + [Trait("Integration", "Integration")] + public async void CanConfigureStorage() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + +// //For some reason, the tests cache something on circleci and this is set by default +// //Initially, there is no configuration, make sure we display the choices available to configure +// Assert.IsType(Assert.IsType(await controller.Storage()).Model); +// +// //the file list should tell us it's not configured: +// var viewFilesViewModelInitial = +// Assert.IsType(Assert.IsType(await controller.Files()).Model); +// Assert.False(viewFilesViewModelInitial.StorageConfigured); + + + //Once we select a provider, redirect to its view + var localResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.FileSystem + })); + Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName); + Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]); + +// +// var AmazonS3result = Assert +// .IsType(controller.Storage(new StorageSettings() +// { +// Provider = StorageProvider.AmazonS3 +// })); +// Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName); +// Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]); +// +// var GoogleResult = Assert +// .IsType(controller.Storage(new StorageSettings() +// { +// Provider = StorageProvider.GoogleCloudStorage +// })); +// Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName); +// Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]); +// + + var AzureResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.AzureBlobStorage + })); + Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName); + Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]); + + //Cool, we get redirected to the config pages + //Let's configure this stuff + + //Let's try and cheat and go to an invalid storage provider config + Assert.Equal(nameof(Storage), (Assert + .IsType(await controller.StorageProvider("I am not a real provider")) + .ActionName)); + + //ok no more messing around, let's configure this shit. + var fileSystemStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) + .Model); + + //local file system does not need config, easy days! + Assert.IsType( + await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); + + //ok cool, let's see if this got set right + var shouldBeRedirectingToLocalStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.FileSystem, + shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); + + + //if we tell the settings page to force, it should allow us to select a new provider + Assert.IsType(Assert.IsType(await controller.Storage(true)).Model); + + //awesome, now let's see if the files result says we're all set up + var viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files()).Model); + Assert.True(viewFilesViewModel.StorageConfigured); + Assert.Empty(viewFilesViewModel.Files); + } + } + + [Fact] + [Trait("Integration", "Integration")] + public async void CanUseLocalProviderFiles() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + var fileSystemStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) + .Model); + Assert.IsType( + await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); + + var shouldBeRedirectingToLocalStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.FileSystem, + shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); + + + await CanUploadRemoveFiles(controller); + } + } + + [Fact] + [Trait("ExternalIntegration", "ExternalIntegration")] + public async Task CanUseAzureBlobStorage() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + var azureBlobStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) + .Model); + + azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString"); + azureBlobStorageConfiguration.ContainerName = "testscontainer"; + Assert.IsType( + await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration)); + + + var shouldBeRedirectingToAzureStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.AzureBlobStorage, + shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]); + + //seems like azure config worked, let's see if the conn string was actually saved + + Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert + .IsType(Assert + .IsType( + await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) + .Model).ConnectionString); + + + + await CanUploadRemoveFiles(controller); + } + } + + + private async Task CanUploadRemoveFiles(ServerController controller) + { + var filename = "uploadtestfile.txt"; + var fileContent = "content"; + File.WriteAllText(filename, fileContent); + + var fileInfo = new FileInfo(filename); + var formFile = new FormFile( + new FileStream(filename, FileMode.OpenOrCreate), + 0, + fileInfo.Length, fileInfo.Name, fileInfo.Name) + { + Headers = new HeaderDictionary() + }; + formFile.ContentType = "text/plain"; + formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\""; + var uploadFormFileResult = Assert.IsType(await controller.CreateFile(formFile)); + Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId")); + var fileId = uploadFormFileResult.RouteValues["fileId"].ToString(); + Assert.Equal("Files", uploadFormFileResult.ActionName); + + var viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files(fileId)).Model); + + Assert.NotEmpty(viewFilesViewModel.Files); + Assert.Equal(fileId, viewFilesViewModel.SelectedFileId); + Assert.NotEmpty(viewFilesViewModel.DirectFileUrl); + + + var net = new System.Net.WebClient(); + var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl)); + Assert.Equal(fileContent, data); + + Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert + .IsType(await controller.DeleteFile(fileId)) + .RouteValues["statusMessage"].ToString()).Severity); + + viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files(fileId)).Model); + + Assert.Null(viewFilesViewModel.DirectFileUrl); + Assert.Null(viewFilesViewModel.SelectedFileId); + } + + + + private static string GetFromSecrets(string key) + { + var builder = new ConfigurationBuilder(); + builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117"); + var config = builder.Build(); + var token = config[key]; + Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} \""); + return token; + } + } +} diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index a9aa57a24..62d3dce8d 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -70,6 +70,11 @@ + + + + + diff --git a/BTCPayServer/Controllers/ServerController.Storage.cs b/BTCPayServer/Controllers/ServerController.Storage.cs new file mode 100644 index 000000000..8bbf2348a --- /dev/null +++ b/BTCPayServer/Controllers/ServerController.Storage.cs @@ -0,0 +1,289 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Models; +using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.AmazonS3Storage; +using BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration; +using BTCPayServer.Storage.Services.Providers.AzureBlobStorage; +using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage; +using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration; +using BTCPayServer.Storage.Services.Providers.Models; +using BTCPayServer.Storage.ViewModels; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Controllers +{ + public partial class ServerController + { + [HttpGet("server/files/{fileId?}")] + public async Task Files(string fileId = null, string statusMessage = null) + { + TempData["StatusMessage"] = statusMessage; + var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(fileId); + + return View(new ViewFilesViewModel() + { + Files = await _StoredFileRepository.GetFiles(), + SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId, + DirectFileUrl = fileUrl, + StorageConfigured = (await _SettingsRepository.GetSettingAsync()) != null + }); + } + + [HttpGet("server/files/{fileId}/delete")] + public async Task DeleteFile(string fileId) + { + try + { + await _FileService.RemoveFile(fileId, null); + return RedirectToAction(nameof(Files), new + { + fileId = "", + statusMessage = "File removed" + }); + } + catch (Exception e) + { + return RedirectToAction(nameof(Files), new + { + statusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = e.Message + } + }); + } + } + + [HttpGet("server/files/{fileId}/tmp")] + public async Task CreateTemporaryFileUrl(string fileId) + { + var file = await _StoredFileRepository.GetFile(fileId); + + if (file == null) + { + return NotFound(); + } + + return View(new CreateTemporaryFileUrlViewModel()); + } + + [HttpPost("server/files/{fileId}/tmp")] + public async Task CreateTemporaryFileUrl(string fileId, + CreateTemporaryFileUrlViewModel viewModel) + { + if (viewModel.TimeAmount <= 0) + { + ModelState.AddModelError(nameof(viewModel.TimeAmount), "Time must be at least 1"); + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var file = await _StoredFileRepository.GetFile(fileId); + + if (file == null) + { + return NotFound(); + } + + var expiry = DateTimeOffset.Now; + switch (viewModel.TimeType) + { + case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds: + expiry =expiry.AddSeconds(viewModel.TimeAmount); + break; + case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes: + expiry = expiry.AddMinutes(viewModel.TimeAmount); + break; + case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Hours: + expiry = expiry.AddHours(viewModel.TimeAmount); + break; + case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Days: + expiry = expiry.AddDays(viewModel.TimeAmount); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var url = await _FileService.GetTemporaryFileUrl(fileId, expiry, viewModel.IsDownload); + + return RedirectToAction(nameof(Files), new + { + StatusMessage = new StatusMessageModel() + { + Html = + $"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. {url}" + }.ToString(), + fileId, + }); + + } + + public class CreateTemporaryFileUrlViewModel + { + public enum TmpFileTimeType + { + Seconds, + Minutes, + Hours, + Days + } + public int TimeAmount { get; set; } + public TmpFileTimeType TimeType { get; set; } + public bool IsDownload { get; set; } + } + + + [HttpPost("server/files/upload")] + public async Task CreateFile(IFormFile file) + { + var newFile = await _FileService.AddFile(file, GetUserId()); + return RedirectToAction(nameof(Files), new + { + statusMessage = "File added!", + fileId = newFile.Id + }); + } + + private string GetUserId() + { + return _UserManager.GetUserId(ControllerContext.HttpContext.User); + } + + [HttpGet("server/storage")] + public async Task Storage(bool forceChoice = false, string statusMessage = null) + { + TempData["StatusMessage"] = statusMessage; + var savedSettings = await _SettingsRepository.GetSettingAsync(); + if (forceChoice || savedSettings == null) + { + return View(new ChooseStorageViewModel() + { + ShowChangeWarning = savedSettings != null, + Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem + }); + } + + return RedirectToAction(nameof(StorageProvider), new + { + provider = savedSettings.Provider + }); + } + + [HttpPost("server/storage")] + public IActionResult Storage(StorageSettings viewModel) + { + return RedirectToAction("StorageProvider", "Server", new + { + provider = viewModel.Provider.ToString() + }); + } + + [HttpGet("server/storage/{provider}")] + public async Task StorageProvider(string provider) + { + if (!Enum.TryParse(typeof(StorageProvider), provider, out var storageProvider)) + { + return RedirectToAction(nameof(Storage), new + { + StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = $"{provider} provider is not supported" + }.ToString() + }); + } + + var data = (await _SettingsRepository.GetSettingAsync()) ?? new StorageSettings(); + + var storageProviderService = + _StorageProviderServices.SingleOrDefault(service => service.StorageProvider().Equals(storageProvider)); + + switch (storageProviderService) + { + case null: + return RedirectToAction(nameof(Storage), new + { + StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = $"{storageProvider} is not supported" + }.ToString() + }); + case AzureBlobStorageFileProviderService fileProviderService: + return View(nameof(EditAzureBlobStorageStorageProvider), + fileProviderService.GetProviderConfiguration(data)); + +// case AmazonS3FileProviderService fileProviderService: +// return View(nameof(EditAmazonS3StorageProvider), +// fileProviderService.GetProviderConfiguration(data)); +// +// case GoogleCloudStorageFileProviderService fileProviderService: +// return View(nameof(EditGoogleCloudStorageStorageProvider), +// fileProviderService.GetProviderConfiguration(data)); + + case FileSystemFileProviderService fileProviderService: + return View(nameof(EditFileSystemStorageProvider), + fileProviderService.GetProviderConfiguration(data)); + } + + return NotFound(); + } + + + [HttpPost("server/storage/AzureBlobStorage")] + public async Task EditAzureBlobStorageStorageProvider(AzureBlobStorageConfiguration viewModel) + { + return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AzureBlobStorage); + } + +// [HttpPost("server/storage/AmazonS3")] +// public async Task EditAmazonS3StorageProvider(AmazonS3StorageConfiguration viewModel) +// { +// return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3); +// } +// +// [HttpPost("server/storage/GoogleCloudStorage")] +// public async Task EditGoogleCloudStorageStorageProvider( +// GoogleCloudStorageConfiguration viewModel) +// { +// return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage); +// } + + [HttpPost("server/storage/FileSystem")] + public async Task EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel) + { + return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.FileSystem); + } + + private async Task SaveStorageProvider(IBaseStorageConfiguration viewModel, + StorageProvider storageProvider) + { + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var data = (await _SettingsRepository.GetSettingAsync()) ?? new StorageSettings(); + data.Provider = storageProvider; + data.Configuration = JObject.FromObject(viewModel); + await _SettingsRepository.UpdateSetting(data); + TempData["StatusMessage"] = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Success, + Message = "Storage settings updated successfully" + }.ToString(); + return View(viewModel); + } + } +} diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index c3ffc13e0..d9d01cb43 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -27,6 +27,9 @@ using Renci.SshNet; using BTCPayServer.Logging; using BTCPayServer.Lightning; using System.Runtime.CompilerServices; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services; +using BTCPayServer.Storage.Services.Providers; using BTCPayServer.Services.Apps; using Microsoft.AspNetCore.Mvc.Rendering; using BTCPayServer.Data; @@ -34,7 +37,7 @@ using BTCPayServer.Data; namespace BTCPayServer.Controllers { [Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)] - public class ServerController : Controller + public partial class ServerController : Controller { private UserManager _UserManager; SettingsRepository _SettingsRepository; @@ -45,8 +48,14 @@ namespace BTCPayServer.Controllers private readonly TorServices _torServices; BTCPayServerOptions _Options; ApplicationDbContextFactory _ContextFactory; + private readonly StoredFileRepository _StoredFileRepository; + private readonly FileService _FileService; + private readonly IEnumerable _StorageProviderServices; public ServerController(UserManager userManager, + StoredFileRepository storedFileRepository, + FileService fileService, + IEnumerable storageProviderServices, BTCPayServerOptions options, RateFetcher rateProviderFactory, SettingsRepository settingsRepository, @@ -58,6 +67,9 @@ namespace BTCPayServer.Controllers ApplicationDbContextFactory contextFactory) { _Options = options; + _StoredFileRepository = storedFileRepository; + _FileService = fileService; + _StorageProviderServices = storageProviderServices; _UserManager = userManager; _SettingsRepository = settingsRepository; _dashBoard = dashBoard; @@ -490,7 +502,7 @@ namespace BTCPayServer.Controllers } [Route("server/services")] - public IActionResult Services() + public async Task Services() { var result = new ServicesViewModel(); result.ExternalServices = _Options.ExternalServices; @@ -529,6 +541,13 @@ namespace BTCPayServer.Controllers }); } } + + var storageSettings = await _SettingsRepository.GetSettingAsync(); + result.ExternalStorageServices.Add(new ServicesViewModel.OtherExternalService() + { + Name = storageSettings == null? "Not set": storageSettings.Provider.ToString(), + Link = Url.Action("Storage") + }); return View(result); } diff --git a/BTCPayServer/Controllers/StorageController.cs b/BTCPayServer/Controllers/StorageController.cs new file mode 100644 index 000000000..4bac3beac --- /dev/null +++ b/BTCPayServer/Controllers/StorageController.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using BTCPayServer.Storage.Services; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Storage +{ + [Route("Storage")] + public class StorageController + { + private readonly FileService _FileService; + + public StorageController(FileService fileService) + { + _FileService = fileService; + } + + [HttpGet("{fileId}")] + public async Task GetFile(string fileId) + { + var url = await _FileService.GetFileUrl(fileId); + return new RedirectResult(url); + } + } +} diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index 53cfc1c32..169129606 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Linq; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using BTCPayServer.Models; using BTCPayServer.Services.PaymentRequests; -using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using BTCPayServer.Storage.Models; using Microsoft.EntityFrameworkCore.Infrastructure; namespace BTCPayServer.Data @@ -93,6 +90,11 @@ namespace BTCPayServer.Data } public DbSet ApiKeys + { + get; set; + } + + public DbSet Files { get; set; } diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 02340fd57..d0165ea6e 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -36,6 +36,10 @@ using System.Net; using BTCPayServer.PaymentRequest; using BTCPayServer.Security; using BTCPayServer.Services.Apps; +using BTCPayServer.Storage; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; namespace BTCPayServer.Hosting { @@ -64,6 +68,7 @@ namespace BTCPayServer.Hosting .AddDefaultTokenProviders(); services.AddSignalR(); services.AddBTCPayServer(); + services.AddProviderStorage(); services.AddSession(); services.AddMvc(o => { @@ -169,6 +174,7 @@ namespace BTCPayServer.Hosting app.UseCors(); app.UsePayServer(); app.UseStaticFiles(); + app.UseProviderStorage(options); app.UseAuthentication(); app.UseSession(); app.UseSignalR(route => diff --git a/BTCPayServer/Migrations/20190324141717_AddFiles.Designer.cs b/BTCPayServer/Migrations/20190324141717_AddFiles.Designer.cs new file mode 100644 index 000000000..a937db748 --- /dev/null +++ b/BTCPayServer/Migrations/20190324141717_AddFiles.Designer.cs @@ -0,0 +1,635 @@ +// +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20190324141717_AddFiles")] + partial class AddFiles + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.8-servicing-32085"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("CreatedTime"); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(50); + + b.Property("StoreId") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AppType"); + + b.Property("Created"); + + b.Property("Name"); + + b.Property("Settings"); + + b.Property("StoreDataId"); + + b.Property("TagAllInvoices"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId"); + + b.Property("Address"); + + b.Property("Assigned"); + + b.Property("CryptoCode"); + + b.Property("UnAssigned"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId"); + + b.Property("UniqueId"); + + b.Property("Message"); + + b.Property("Timestamp"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Accounted"); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id"); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DefaultCrypto"); + + b.Property("DerivationStrategies"); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreBlob"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("FileName"); + + b.Property("StorageFileName"); + + b.Property("Timestamp"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("APIKeys") + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Apps") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Invoices") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Events") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PairedSINs") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("PendingInvoices") + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PaymentRequests") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20190324141717_AddFiles.cs b/BTCPayServer/Migrations/20190324141717_AddFiles.cs new file mode 100644 index 000000000..f7833ec6d --- /dev/null +++ b/BTCPayServer/Migrations/20190324141717_AddFiles.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class AddFiles : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + FileName = table.Column(nullable: true), + StorageFileName = table.Column(nullable: true), + Timestamp = table.Column(nullable: false), + ApplicationUserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + table.ForeignKey( + name: "FK_Files_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Files_ApplicationUserId", + table: "Files", + column: "ApplicationUserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Files"); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 6ac586aca..919b92944 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -348,6 +348,26 @@ namespace BTCPayServer.Migrations b.ToTable("PaymentRequests"); }); + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("FileName"); + + b.Property("StorageFileName"); + + b.Property("Timestamp"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") @@ -556,6 +576,13 @@ namespace BTCPayServer.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") diff --git a/BTCPayServer/Models/ApplicationUser.cs b/BTCPayServer/Models/ApplicationUser.cs index d5c04ccad..31360f52c 100644 --- a/BTCPayServer/Models/ApplicationUser.cs +++ b/BTCPayServer/Models/ApplicationUser.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using BTCPayServer.Data; +using BTCPayServer.Storage.Models; namespace BTCPayServer.Models { @@ -20,5 +21,11 @@ namespace BTCPayServer.Models { get; set; } + + public List StoredFiles + { + get; + set; + } } } diff --git a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs index d29a6ac5a..7866b4525 100644 --- a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs @@ -20,5 +20,6 @@ namespace BTCPayServer.Models.ServerViewModels public List OtherExternalServices { get; set; } = new List(); public List TorHttpServices { get; set; } = new List(); public List TorOtherServices { get; set; } = new List(); + public List ExternalStorageServices { get; set; } = new List(); } } diff --git a/BTCPayServer/Models/ServerViewModels/ViewFilesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ViewFilesViewModel.cs new file mode 100644 index 000000000..0af9fc88b --- /dev/null +++ b/BTCPayServer/Models/ServerViewModels/ViewFilesViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using BTCPayServer.Storage.Models; + +namespace BTCPayServer.Models.ServerViewModels +{ + public class ViewFilesViewModel + { + public List Files { get; set; } + public string DirectFileUrl { get; set; } + public string SelectedFileId { get; set; } + public bool StorageConfigured { get; set; } + } +} diff --git a/BTCPayServer/Storage/Models/StorageProvider.cs b/BTCPayServer/Storage/Models/StorageProvider.cs new file mode 100644 index 000000000..0858513d3 --- /dev/null +++ b/BTCPayServer/Storage/Models/StorageProvider.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Storage.Models +{ + public enum StorageProvider + { + AzureBlobStorage=0, +// AmazonS3 =1, +// GoogleCloudStorage =2, + FileSystem =3 + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Models/StorageSettings.cs b/BTCPayServer/Storage/Models/StorageSettings.cs new file mode 100644 index 000000000..5add18e0d --- /dev/null +++ b/BTCPayServer/Storage/Models/StorageSettings.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Storage.Models +{ + public class StorageSettings + { + public StorageProvider Provider { get; set; } + public string ConfigurationStr { get; set; } + + [NotMapped] + public JObject Configuration + { + get => JsonConvert.DeserializeObject(string.IsNullOrEmpty(ConfigurationStr) ? "{}" : ConfigurationStr); + set => ConfigurationStr = value.ToString(); + } + } +} diff --git a/BTCPayServer/Storage/Models/StoredFile.cs b/BTCPayServer/Storage/Models/StoredFile.cs new file mode 100644 index 000000000..6e7e668cd --- /dev/null +++ b/BTCPayServer/Storage/Models/StoredFile.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; +using BTCPayServer.Models; + +namespace BTCPayServer.Storage.Models +{ + public class StoredFile + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + public string FileName { get; set; } + public string StorageFileName { get; set; } + public DateTime Timestamp { get; set; } + public string ApplicationUserId { get; set; } + public ApplicationUser ApplicationUser + { + get; set; + } + } +} diff --git a/BTCPayServer/Storage/Services/FileService.cs b/BTCPayServer/Storage/Services/FileService.cs new file mode 100644 index 000000000..3e5e1d95a --- /dev/null +++ b/BTCPayServer/Storage/Services/FileService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers; +using Microsoft.AspNetCore.Http; + +namespace BTCPayServer.Storage.Services +{ + public class FileService + { + private readonly StoredFileRepository _FileRepository; + private readonly IEnumerable _providers; + private readonly SettingsRepository _SettingsRepository; + + public FileService(StoredFileRepository fileRepository, IEnumerable providers, + SettingsRepository settingsRepository) + { + _FileRepository = fileRepository; + _providers = providers; + _SettingsRepository = settingsRepository; + } + + public async Task AddFile(IFormFile file, string userId) + { + var settings = await _SettingsRepository.GetSettingAsync(); + var provider = GetProvider(settings); + + var storedFile = await provider.AddFile(file, settings); + storedFile.ApplicationUserId = userId; + await _FileRepository.AddFile(storedFile); + return storedFile; + } + + public async Task GetFileUrl(string fileId) + { + var settings = await _SettingsRepository.GetSettingAsync(); + var provider = GetProvider(settings); + var storedFile = await _FileRepository.GetFile(fileId); + return storedFile == null ? null: await provider.GetFileUrl(storedFile, settings); + } + + public async Task GetTemporaryFileUrl(string fileId, DateTimeOffset expiry, bool isDownload) + { + var settings = await _SettingsRepository.GetSettingAsync(); + var provider = GetProvider(settings); + var storedFile = await _FileRepository.GetFile(fileId); + return storedFile == null ? null: await provider.GetTemporaryFileUrl(storedFile, settings,expiry,isDownload); + } + + public async Task RemoveFile(string fileId, string userId) + { + var settings = await _SettingsRepository.GetSettingAsync(); + var provider = GetProvider(settings); + var storedFile = await _FileRepository.GetFile(fileId); + if (string.IsNullOrEmpty(userId) || + storedFile.ApplicationUserId.Equals(userId, StringComparison.InvariantCultureIgnoreCase)) + { + await provider.RemoveFile(storedFile, settings); + await _FileRepository.RemoveFile(storedFile); + } + } + + private IStorageProviderService GetProvider(StorageSettings storageSettings) + { + return _providers.FirstOrDefault((service) => service.StorageProvider().Equals(storageSettings.Provider)); + } + } +} diff --git a/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/AmazonS3FileProviderService.cs b/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/AmazonS3FileProviderService.cs new file mode 100644 index 000000000..59b17f467 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/AmazonS3FileProviderService.cs @@ -0,0 +1,27 @@ +//using System.Threading.Tasks; +//using BTCPayServer.Storage.Models; +//using BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration; +//using TwentyTwenty.Storage; +//using TwentyTwenty.Storage.Amazon; +// +//namespace BTCPayServer.Storage.Services.Providers.AmazonS3Storage +//{ +// public class +// AmazonS3FileProviderService : BaseTwentyTwentyStorageFileProviderServiceBase +// { +// public override StorageProvider StorageProvider() +// { +// return Storage.Models.StorageProvider.AmazonS3; +// } +// +// public override AmazonS3StorageConfiguration GetProviderConfiguration(StorageSettings configuration) +// { +// return configuration.Configuration.ParseAmazonS3StorageConfiguration(); +// } +// +// protected override Task GetStorageProvider(AmazonS3StorageConfiguration configuration) +// { +// return Task.FromResult(new AmazonStorageProvider(configuration)); +// } +// } +//} diff --git a/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/Configuration/AmazonS3FileProviderServiceExtensions.cs b/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/Configuration/AmazonS3FileProviderServiceExtensions.cs new file mode 100644 index 000000000..5b5565ead --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/Configuration/AmazonS3FileProviderServiceExtensions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration +{ + public static class AmazonS3FileProviderServiceExtensions + { + public static AmazonS3StorageConfiguration ParseAmazonS3StorageConfiguration(this JObject jObject) + { + return jObject.ToObject(); + } + + public static JObject ConvertConfiguration(this AmazonS3StorageConfiguration configuration) + { + return JObject.FromObject(configuration); + } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/Configuration/AmazonS3StorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/Configuration/AmazonS3StorageConfiguration.cs new file mode 100644 index 000000000..b6ac0a878 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AmazonS3Storage/Configuration/AmazonS3StorageConfiguration.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Storage.Services.Providers.Models; +using TwentyTwenty.Storage.Amazon; + +namespace BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration + { + public class AmazonS3StorageConfiguration : AmazonProviderOptions, IBaseStorageConfiguration + { + public string ContainerName { get; set; } + } + } \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/AzureBlobStorageFileProviderService.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/AzureBlobStorageFileProviderService.cs new file mode 100644 index 000000000..899e94f35 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/AzureBlobStorageFileProviderService.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; +using TwentyTwenty.Storage; +using TwentyTwenty.Storage.Azure; + +namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage +{ + public class + AzureBlobStorageFileProviderService : BaseTwentyTwentyStorageFileProviderServiceBase< + AzureBlobStorageConfiguration> + { + public override StorageProvider StorageProvider() + { + return Storage.Models.StorageProvider.AzureBlobStorage; + } + + public override AzureBlobStorageConfiguration GetProviderConfiguration(StorageSettings configuration) + { + return configuration.Configuration.ParseAzureBlobStorageConfiguration(); + } + + protected override Task GetStorageProvider(AzureBlobStorageConfiguration configuration) + { + return Task.FromResult(new AzureStorageProvider(configuration)); + } + } +} diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs new file mode 100644 index 000000000..c6ed86594 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using BTCPayServer.Storage.Services.Providers.Models; +using TwentyTwenty.Storage.Azure; + +namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration +{ + public class AzureBlobStorageConfiguration : AzureProviderOptions, IBaseStorageConfiguration + { + [Required] + [MinLength(3)] + [MaxLength(63)] + [RegularExpression(@"[a-z0-9-]+", + ErrorMessage = "Characters must be lowercase or digits or -")] + public string ContainerName { get; set; } + } +} diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageFileProviderServiceExtensions.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageFileProviderServiceExtensions.cs new file mode 100644 index 000000000..62a1f9338 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageFileProviderServiceExtensions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration +{ + public static class AzureBlobStorageFileProviderServiceExtensions + { + public static AzureBlobStorageConfiguration ParseAzureBlobStorageConfiguration(this JObject jObject) + { + return jObject.ToObject(); + } + + public static JObject ConvertConfiguration(this AzureBlobStorageConfiguration configuration) + { + return JObject.FromObject(configuration); + } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs b/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs new file mode 100644 index 000000000..5ccfd7646 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.Models; +using Microsoft.AspNetCore.Http; +using TwentyTwenty.Storage; + +namespace BTCPayServer.Storage.Services.Providers +{ + public abstract class + BaseTwentyTwentyStorageFileProviderServiceBase : IStorageProviderService + where TStorageConfiguration : IBaseStorageConfiguration + { + public abstract StorageProvider StorageProvider(); + + public virtual async Task AddFile(IFormFile file, StorageSettings configuration) + { + //respect https://www.microsoftpressstore.com/articles/article.aspx?p=2224058&seqNum=8 in naming + var storageFileName = $"{Guid.NewGuid()}-{file.FileName.ToLowerInvariant()}"; + var providerConfiguration = GetProviderConfiguration(configuration); + var provider = await GetStorageProvider(providerConfiguration); + using (var fileStream = file.OpenReadStream()) + { + await provider.SaveBlobStreamAsync(providerConfiguration.ContainerName, storageFileName, fileStream, + new BlobProperties() + { + ContentType = file.ContentType, + ContentDisposition = file.ContentDisposition, + Security = BlobSecurity.Public, + }); + } + + return new StoredFile() + { + Timestamp = DateTime.Now, + FileName = file.FileName, + StorageFileName = storageFileName + }; + } + + public virtual async Task GetFileUrl(StoredFile storedFile, StorageSettings configuration) + { + var providerConfiguration = GetProviderConfiguration(configuration); + var provider = await GetStorageProvider(providerConfiguration); + return provider.GetBlobUrl(providerConfiguration.ContainerName, storedFile.StorageFileName); + } + + public virtual async Task GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, + DateTimeOffset expiry, bool isDownload, BlobUrlAccess access = BlobUrlAccess.Read) + { + var providerConfiguration = GetProviderConfiguration(configuration); + var provider = await GetStorageProvider(providerConfiguration); + if (isDownload) + { + var descriptor = + await provider.GetBlobDescriptorAsync(providerConfiguration.ContainerName, + storedFile.StorageFileName); + return provider.GetBlobSasUrl(providerConfiguration.ContainerName, storedFile.StorageFileName, expiry, + true, storedFile.FileName, descriptor.ContentType, access); + } + + return provider.GetBlobSasUrl(providerConfiguration.ContainerName, storedFile.StorageFileName, expiry, + false, null, null, access); + } + + public async Task RemoveFile(StoredFile storedFile, StorageSettings configuration) + { + var providerConfiguration = GetProviderConfiguration(configuration); + var provider = await GetStorageProvider(providerConfiguration); + await provider.DeleteBlobAsync(providerConfiguration.ContainerName, storedFile.StorageFileName); + } + + public abstract TStorageConfiguration GetProviderConfiguration(StorageSettings configuration); + + protected abstract Task GetStorageProvider(TStorageConfiguration configuration); + } +} diff --git a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/Configuration/FileSystemFileProviderServiceExtensions.cs b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/Configuration/FileSystemFileProviderServiceExtensions.cs new file mode 100644 index 000000000..b8630164c --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/Configuration/FileSystemFileProviderServiceExtensions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration +{ + public static class FileSystemFileProviderServiceExtensions + { + public static FileSystemStorageConfiguration ParseFileSystemStorageConfiguration(this JObject jObject) + { + return jObject.ToObject(); + } + + public static JObject ConvertConfiguration(this FileSystemStorageConfiguration configuration) + { + return JObject.FromObject(configuration); + } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/Configuration/FileSystemStorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/Configuration/FileSystemStorageConfiguration.cs new file mode 100644 index 000000000..4bb7ff39f --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/Configuration/FileSystemStorageConfiguration.cs @@ -0,0 +1,9 @@ +using BTCPayServer.Storage.Services.Providers.Models; + +namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration +{ + public class FileSystemStorageConfiguration : IBaseStorageConfiguration + { + public string ContainerName { get; set; } = string.Empty; + } +} diff --git a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs new file mode 100644 index 000000000..cfeb7880e --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Services; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using TwentyTwenty.Storage; +using TwentyTwenty.Storage.Local; + +namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage +{ + public class + FileSystemFileProviderService : BaseTwentyTwentyStorageFileProviderServiceBase + { + private readonly BTCPayServerEnvironment _BtcPayServerEnvironment; + private readonly BTCPayServerOptions _Options; + private readonly IHttpContextAccessor _HttpContextAccessor; + + public FileSystemFileProviderService(BTCPayServerEnvironment btcPayServerEnvironment, + BTCPayServerOptions options, IHttpContextAccessor httpContextAccessor) + { + _BtcPayServerEnvironment = btcPayServerEnvironment; + _Options = options; + _HttpContextAccessor = httpContextAccessor; + } + public const string LocalStorageDirectoryName = "LocalStorage"; + + public static string GetStorageDir(BTCPayServerOptions options) + { + return Path.Combine(options.DataDir, LocalStorageDirectoryName); + } + + public override StorageProvider StorageProvider() + { + return Storage.Models.StorageProvider.FileSystem; + } + + public override FileSystemStorageConfiguration GetProviderConfiguration(StorageSettings configuration) + { + return configuration.Configuration.ParseFileSystemStorageConfiguration(); + } + + protected override Task GetStorageProvider(FileSystemStorageConfiguration configuration) + { + return Task.FromResult( + new LocalStorageProvider(new DirectoryInfo(GetStorageDir(_Options)).FullName)); + } + + public override async Task GetFileUrl(StoredFile storedFile, StorageSettings configuration) + { + var baseResult = await base.GetFileUrl(storedFile, configuration); + var url = + _HttpContextAccessor.HttpContext.Request.IsOnion() + ? _BtcPayServerEnvironment.OnionUrl + : $"{_BtcPayServerEnvironment.ExpectedProtocol}://" + + $"{_BtcPayServerEnvironment.ExpectedHost}" + + $"{_Options.RootPath}{LocalStorageDirectoryName}"; + return baseResult.Replace(new DirectoryInfo(GetStorageDir(_Options)).FullName, url, + StringComparison.InvariantCultureIgnoreCase); + } + + public override Task GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, DateTimeOffset expiry, bool isDownload, + BlobUrlAccess access = BlobUrlAccess.Read) + { + return GetFileUrl(storedFile, configuration); + } + } +} diff --git a/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/Configuration/GoogleCloudStorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/Configuration/GoogleCloudStorageConfiguration.cs new file mode 100644 index 000000000..f624381cf --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/Configuration/GoogleCloudStorageConfiguration.cs @@ -0,0 +1,11 @@ +using BTCPayServer.Storage.Services.Providers.Models; +using TwentyTwenty.Storage.Google; + +namespace BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration +{ + public class GoogleCloudStorageConfiguration : GoogleProviderOptions, IBaseStorageConfiguration + { + public string JsonCredentials{ get; set; } + public string ContainerName { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/Configuration/GoogleCloudStorageFileProviderServiceExtensions.cs b/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/Configuration/GoogleCloudStorageFileProviderServiceExtensions.cs new file mode 100644 index 000000000..91bfe4e6e --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/Configuration/GoogleCloudStorageFileProviderServiceExtensions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration +{ + public static class GoogleCloudStorageFileProviderServiceExtensions + { + public static GoogleCloudStorageConfiguration ParseGoogleCloudStorageConfiguration(this JObject jObject) + { + return jObject.ToObject(); + } + + public static JObject ConvertConfiguration(this GoogleCloudStorageConfiguration configuration) + { + return JObject.FromObject(configuration); + } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/GoogleCloudStorageFileProviderService.cs b/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/GoogleCloudStorageFileProviderService.cs new file mode 100644 index 000000000..d6fef8d1d --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/GoogleCloudStorage/GoogleCloudStorageFileProviderService.cs @@ -0,0 +1,30 @@ +//using System.Threading.Tasks; +//using BTCPayServer.Storage.Models; +//using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration; +//using Google.Apis.Auth.OAuth2; +//using TwentyTwenty.Storage; +//using TwentyTwenty.Storage.Google; +// +//namespace BTCPayServer.Storage.Services.Providers.GoogleCloudStorage +//{ +// public class +// GoogleCloudStorageFileProviderService : BaseTwentyTwentyStorageFileProviderServiceBase< +// GoogleCloudStorageConfiguration> +// { +// public override StorageProvider StorageProvider() +// { +// return Storage.Models.StorageProvider.GoogleCloudStorage; +// } +// +// public override GoogleCloudStorageConfiguration GetProviderConfiguration(StorageSettings configuration) +// { +// return configuration.Configuration.ParseGoogleCloudStorageConfiguration(); +// } +// +// protected override Task GetStorageProvider( +// GoogleCloudStorageConfiguration configuration) +// { +// return Task.FromResult(new GoogleStorageProvider(GoogleCredential.FromJson(configuration.JsonCredentials), configuration)); +// } +// } +//} diff --git a/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs b/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs new file mode 100644 index 000000000..770678001 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Storage.Models; +using Microsoft.AspNetCore.Http; +using TwentyTwenty.Storage; + +namespace BTCPayServer.Storage.Services.Providers +{ + public interface IStorageProviderService + { + Task AddFile(IFormFile formFile, StorageSettings configuration); + Task RemoveFile(StoredFile storedFile, StorageSettings configuration); + Task GetFileUrl(StoredFile storedFile, StorageSettings configuration); + Task GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, + DateTimeOffset expiry, bool isDownload, BlobUrlAccess access = BlobUrlAccess.Read); + StorageProvider StorageProvider(); + } +} diff --git a/BTCPayServer/Storage/Services/Providers/Models/IBaseStorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/Models/IBaseStorageConfiguration.cs new file mode 100644 index 000000000..5ed2bf92d --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/Models/IBaseStorageConfiguration.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Storage.Services.Providers.Models +{ + public interface IBaseStorageConfiguration + { + string ContainerName { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/StoredFileRepository.cs b/BTCPayServer/Storage/Services/StoredFileRepository.cs new file mode 100644 index 000000000..476dbf531 --- /dev/null +++ b/BTCPayServer/Storage/Services/StoredFileRepository.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Storage.Models; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Storage.Services +{ + public class StoredFileRepository + { + private readonly ApplicationDbContextFactory _ApplicationDbContextFactory; + + public StoredFileRepository(ApplicationDbContextFactory applicationDbContextFactory) + { + _ApplicationDbContextFactory = applicationDbContextFactory; + } + + public async Task GetFile(string fileId) + { + var filesResult = await GetFiles(new FilesQuery() {Id = new string[] {fileId}}); + return filesResult.FirstOrDefault(); + } + + public async Task> GetFiles(FilesQuery filesQuery = null) + { + if (filesQuery == null) + { + filesQuery = new FilesQuery(); + } + + using (var context = _ApplicationDbContextFactory.CreateContext()) + { + return await context.Files + .Include(file => file.ApplicationUser) + .Where(file => + (!filesQuery.Id.Any() || filesQuery.Id.Contains(file.Id)) && + (!filesQuery.UserIds.Any() || filesQuery.UserIds.Contains(file.ApplicationUserId))) + .ToListAsync(); + } + } + + public async Task RemoveFile(StoredFile file) + { + using (var context = _ApplicationDbContextFactory.CreateContext()) + { + context.Attach(file); + context.Files.Remove(file); + await context.SaveChangesAsync(); + } + } + + public async Task AddFile(StoredFile storedFile) + { + using (var context = _ApplicationDbContextFactory.CreateContext()) + { + await context.AddAsync(storedFile); + await context.SaveChangesAsync(); + } + } + + public class FilesQuery + { + public string[] Id { get; set; } = Array.Empty(); + public string[] UserIds { get; set; } = Array.Empty(); + } + } +} diff --git a/BTCPayServer/Storage/StorageExtensions.cs b/BTCPayServer/Storage/StorageExtensions.cs new file mode 100644 index 000000000..9cc9a56e4 --- /dev/null +++ b/BTCPayServer/Storage/StorageExtensions.cs @@ -0,0 +1,50 @@ +using System.IO; +using BTCPayServer.Configuration; +using BTCPayServer.Storage.Services; +using BTCPayServer.Storage.Services.Providers; +using BTCPayServer.Storage.Services.Providers.AmazonS3Storage; +using BTCPayServer.Storage.Services.Providers.AzureBlobStorage; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage; +using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + +namespace BTCPayServer.Storage +{ + public static class StorageExtensions + { + public static void AddProviderStorage(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); +// serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); +// serviceCollection.AddSingleton(); + } + + public static void UseProviderStorage(this IApplicationBuilder builder, BTCPayServerOptions options) + { + var dir = FileSystemFileProviderService.GetStorageDir(options); + + DirectoryInfo dirInfo; + if (!Directory.Exists(dir)) + { + dirInfo = Directory.CreateDirectory(dir); + } + else + { + dirInfo = new DirectoryInfo(dir); + } + + builder.UseStaticFiles(new StaticFileOptions() + { + ServeUnknownFileTypes = true, + RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}"), + FileProvider = new PhysicalFileProvider(dirInfo.FullName) + }); + } + } +} diff --git a/BTCPayServer/Storage/ViewModels/ChooseStorageViewModel.cs b/BTCPayServer/Storage/ViewModels/ChooseStorageViewModel.cs new file mode 100644 index 000000000..9346f7aea --- /dev/null +++ b/BTCPayServer/Storage/ViewModels/ChooseStorageViewModel.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Storage.Models; + +namespace BTCPayServer.Storage.ViewModels +{ + public class ChooseStorageViewModel + { + public StorageProvider Provider { get; set; } + public bool ShowChangeWarning { get; set; } + } +} diff --git a/BTCPayServer/Views/Server/CreateTemporaryFileUrl.cshtml b/BTCPayServer/Views/Server/CreateTemporaryFileUrl.cshtml new file mode 100644 index 000000000..05f7a0be3 --- /dev/null +++ b/BTCPayServer/Views/Server/CreateTemporaryFileUrl.cshtml @@ -0,0 +1,44 @@ +@using BTCPayServer.Controllers +@model BTCPayServer.Controllers.ServerController.CreateTemporaryFileUrlViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Create temporary file link"); + +} + + +

@ViewData["Title"]

+ +
+
+
+
+
+
+
+
+
+ + + +
+
+ +
+ + +
+ +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/EditAmazonS3StorageProvider.cshtml b/BTCPayServer/Views/Server/EditAmazonS3StorageProvider.cshtml new file mode 100644 index 000000000..ed346125d --- /dev/null +++ b/BTCPayServer/Views/Server/EditAmazonS3StorageProvider.cshtml @@ -0,0 +1,61 @@ +@model BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration.AmazonS3StorageConfiguration +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Amazon S3"); +} + + +

@ViewData["Title"]

+ +
+
+
+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ + + Change Storage provider +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/EditAzureBlobStorageStorageProvider.cshtml b/BTCPayServer/Views/Server/EditAzureBlobStorageStorageProvider.cshtml new file mode 100644 index 000000000..385113bfa --- /dev/null +++ b/BTCPayServer/Views/Server/EditAzureBlobStorageStorageProvider.cshtml @@ -0,0 +1,36 @@ +@model BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration.AzureBlobStorageConfiguration +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Azure Blob Storage"); +} + + +

@ViewData["Title"]

+ +
+
+
+
+
+
+
+
+
+ + + +
+
+ + + +
+ + + Change Storage provider +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/EditFilesystemStorageProvider.cshtml b/BTCPayServer/Views/Server/EditFilesystemStorageProvider.cshtml new file mode 100644 index 000000000..8d589f79c --- /dev/null +++ b/BTCPayServer/Views/Server/EditFilesystemStorageProvider.cshtml @@ -0,0 +1,26 @@ +@model BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration.FileSystemStorageConfiguration +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Local Filesystem"); +} + +

@ViewData["Title"]

+ +
+
+
+
+
+

Nothing to configure here. Data will be saved in the btcpay data directory.

+
+
+
+ + + Change Storage provider +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/EditGoogleCloudStorageStorageProvider.cshtml b/BTCPayServer/Views/Server/EditGoogleCloudStorageStorageProvider.cshtml new file mode 100644 index 000000000..28fa7206f --- /dev/null +++ b/BTCPayServer/Views/Server/EditGoogleCloudStorageStorageProvider.cshtml @@ -0,0 +1,45 @@ +@model BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration.GoogleCloudStorageConfiguration +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Google Cloud Storage"); +} + +

@ViewData["Title"]

+ +
+
+
+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ + + Change Storage provider +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/Files.cshtml b/BTCPayServer/Views/Server/Files.cshtml new file mode 100644 index 000000000..3e14da2ac --- /dev/null +++ b/BTCPayServer/Views/Server/Files.cshtml @@ -0,0 +1,112 @@ +@model ViewFilesViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Files); +} + + +

@ViewData["Title"]

+ + + + + + + + + + + + + + @foreach (var file in Model.Files) + { + + + + + + + } + @if (!Model.Files.Any()) + { + + + + } + +
NameTimestampUser
@file.FileName@file.Timestamp.ToString("g")@file.ApplicationUser.UserName + Get Link + - Get Temp Link + - Remove +
No files
+ +@if (!string.IsNullOrEmpty(Model.SelectedFileId)) +{ + var file = Model.Files.Single(storedFile => storedFile.Id.Equals(Model.SelectedFileId, StringComparison.InvariantCultureIgnoreCase)); + +} + +@if (Model.StorageConfigured) +{ +
+
+ +
+

Upload File

+ +
+ + +
+ +
+ +
+
+ +@section Scripts { + +} +} diff --git a/BTCPayServer/Views/Server/ServerNavPages.cs b/BTCPayServer/Views/Server/ServerNavPages.cs index bb80f2a02..bf0d098ee 100644 --- a/BTCPayServer/Views/Server/ServerNavPages.cs +++ b/BTCPayServer/Views/Server/ServerNavPages.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server { public enum ServerNavPages { - Index, Users, Rates, Emails, Policies, Theme, Services, Maintenance, Logs + Index, Users, Rates, Emails, Policies, Theme, Services, Maintenance, Logs, Files } } diff --git a/BTCPayServer/Views/Server/Services.cshtml b/BTCPayServer/Views/Server/Services.cshtml index 4135906cc..9878c93a2 100644 --- a/BTCPayServer/Views/Server/Services.cshtml +++ b/BTCPayServer/Views/Server/Services.cshtml @@ -5,7 +5,7 @@

@ViewData["Title"]

- +
@@ -23,23 +23,23 @@
- - - - - + + + + + - @foreach (var s in Model.ExternalServices) - { - - - - - - } + @foreach (var s in Model.ExternalServices) + { + + + + + + }
CryptoAccess TypeActions
CryptoAccess TypeActions
@s.CryptoCode@s.DisplayName - See information -
@s.CryptoCode@s.DisplayName + See information +
@@ -57,21 +57,21 @@
- - - - + + + + - @foreach (var s in Model.OtherExternalServices) - { - - - - - } + @foreach (var s in Model.OtherExternalServices) + { + + + + + }
NameActions
NameActions
@s.Name - See information -
@s.Name + See information +
@@ -90,21 +90,21 @@
- - - - + + + + - @foreach (var s in Model.TorHttpServices) - { - - - - - } + @foreach (var s in Model.TorHttpServices) + { + + + + + }
NameActions
NameActions
@s.Name - See information -
@s.Name + See information +
@@ -123,25 +123,54 @@
- - - - + + + + - @foreach (var s in Model.TorOtherServices) - { - - - - - } + @foreach (var s in Model.TorOtherServices) + { + + + + + }
NameActions
NameActions
@s.Name@s.Link
@s.Name@s.Link
} +
+
+

External storage services

+
+ Integrated Storage providers to store file uploads from btcpay +
+
+ + + + + + + + + @foreach (var s in Model.ExternalStorageServices) + { + + + + + } + +
NameActions
@s.Name + Edit +
+
+
+
@section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") diff --git a/BTCPayServer/Views/Server/Storage.cshtml b/BTCPayServer/Views/Server/Storage.cshtml new file mode 100644 index 000000000..b84830548 --- /dev/null +++ b/BTCPayServer/Views/Server/Storage.cshtml @@ -0,0 +1,36 @@ +@using BTCPayServer.Storage.Models +@model BTCPayServer.Storage.ViewModels.ChooseStorageViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services); +} + + +

@ViewData["Title"]

+ +@if (Model.ShowChangeWarning) +{ +
+ If you change your configured storage provider, your current files will become inaccessible. +
+} +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/_Nav.cshtml b/BTCPayServer/Views/Server/_Nav.cshtml index 32c16abe2..03eecdda6 100644 --- a/BTCPayServer/Views/Server/_Nav.cshtml +++ b/BTCPayServer/Views/Server/_Nav.cshtml @@ -7,5 +7,6 @@ Theme Maintenance Logs + Files