Abstracted cloud storage - Amazon/Google/Azure/Local (#708)

* wip

* add in storage system

* ui fixes

* fix settings ui

* Add Files Crud UI

* add titles

* link files to users

* add migration

* set blob to public

* remove base 64 read code

* fix file query model init

* move view model to own file

* fix local root path

* use datadir for local storage

* move to services

* add direct file url

* try fix tests

* remove magic string

* remove other magic strings

* show error message on unsupported provider

* fix asp net version

* redirect to storage settings if provider is not supported

* start writing tests

* fix tests

* fix test again

* add some more to the tests

* more tests

* try making local provider work on tests

* fix formfile

* fix small issue with returning deleted file

* check if returned data is null for deleted file

* validate azure Container name

* more state fixes

* change azure test trait

* add tmp file url generator

* fix tests

* small clean

* disable amazon and  google


comment out unused code for now


comment out google/amazon
This commit is contained in:
Andrew Camilleri
2019-04-22 09:41:20 +02:00
committed by Nicolas Dorier
parent 02d79de17c
commit b184360eb7
46 changed files with 2392 additions and 61 deletions

View File

@@ -33,4 +33,5 @@
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<ServerController>(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<StorageSettings>(Assert.IsType<ViewResult>(await controller.Storage()).Model);
//
// //the file list should tell us it's not configured:
// var viewFilesViewModelInitial =
// Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
// Assert.False(viewFilesViewModelInitial.StorageConfigured);
//Once we select a provider, redirect to its view
var localResult = Assert
.IsType<RedirectToActionResult>(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<RedirectToActionResult>(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<RedirectToActionResult>(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<RedirectToActionResult>(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<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
.ActionName));
//ok no more messing around, let's configure this shit.
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
//local file system does not need config, easy days!
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
//ok cool, let's see if this got set right
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(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<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
//awesome, now let's see if the files result says we're all set up
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(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<ServerController>(user.UserId, user.StoreId);
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(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<ServerController>(user.UserId, user.StoreId);
var azureBlobStorageConfiguration = Assert.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model);
azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString");
azureBlobStorageConfiguration.ContainerName = "testscontainer";
Assert.IsType<ViewResult>(
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
var shouldBeRedirectingToAzureStorageConfigPage =
Assert.IsType<RedirectToActionResult>(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<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(
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<RedirectToActionResult>(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<ViewFilesViewModel>(Assert.IsType<ViewResult>(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<RedirectToActionResult>(await controller.DeleteFile(fileId))
.RouteValues["statusMessage"].ToString()).Severity);
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(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} <value>\"");
return token;
}
}
}

View File

@@ -70,6 +70,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>

View File

@@ -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<IActionResult> 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<StorageSettings>()) != null
});
}
[HttpGet("server/files/{fileId}/delete")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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}. <a href='{url}' target='_blank'>{url}</a>"
}.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<IActionResult> 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<IActionResult> Storage(bool forceChoice = false, string statusMessage = null)
{
TempData["StatusMessage"] = statusMessage;
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
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<IActionResult> 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<StorageSettings>()) ?? 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<IActionResult> EditAzureBlobStorageStorageProvider(AzureBlobStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AzureBlobStorage);
}
// [HttpPost("server/storage/AmazonS3")]
// public async Task<IActionResult> EditAmazonS3StorageProvider(AmazonS3StorageConfiguration viewModel)
// {
// return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3);
// }
//
// [HttpPost("server/storage/GoogleCloudStorage")]
// public async Task<IActionResult> EditGoogleCloudStorageStorageProvider(
// GoogleCloudStorageConfiguration viewModel)
// {
// return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage);
// }
[HttpPost("server/storage/FileSystem")]
public async Task<IActionResult> EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.FileSystem);
}
private async Task<IActionResult> SaveStorageProvider(IBaseStorageConfiguration viewModel,
StorageProvider storageProvider)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? 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);
}
}
}

View File

@@ -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<ApplicationUser> _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<IStorageProviderService> _StorageProviderServices;
public ServerController(UserManager<ApplicationUser> userManager,
StoredFileRepository storedFileRepository,
FileService fileService,
IEnumerable<IStorageProviderService> 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<IActionResult> Services()
{
var result = new ServicesViewModel();
result.ExternalServices = _Options.ExternalServices;
@@ -529,6 +541,13 @@ namespace BTCPayServer.Controllers
});
}
}
var storageSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
result.ExternalStorageServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = storageSettings == null? "Not set": storageSettings.Provider.ToString(),
Link = Url.Action("Storage")
});
return View(result);
}

View File

@@ -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<IActionResult> GetFile(string fileId)
{
var url = await _FileService.GetFileUrl(fileId);
return new RedirectResult(url);
}
}
}

View File

@@ -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
@@ -97,6 +94,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<StoredFile> Files
{
get; set;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();

View File

@@ -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 =>

View File

@@ -0,0 +1,635 @@
// <auto-generated />
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<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.Property<bool>("TagAllInvoices");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("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<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<string>("FileName");
b.Property<string>("StorageFileName");
b.Property<DateTime>("Timestamp");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -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<string>(nullable: false),
FileName = table.Column<string>(nullable: true),
StorageFileName = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTime>(nullable: false),
ApplicationUserId = table.Column<string>(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");
}
}
}

View File

@@ -348,6 +348,26 @@ namespace BTCPayServer.Migrations
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<string>("FileName");
b.Property<string>("StorageFileName");
b.Property<DateTime>("Timestamp");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")

View File

@@ -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<StoredFile> StoredFiles
{
get;
set;
}
}
}

View File

@@ -20,5 +20,6 @@ namespace BTCPayServer.Models.ServerViewModels
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> TorHttpServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> TorOtherServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> ExternalStorageServices { get; set; } = new List<OtherExternalService>();
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using BTCPayServer.Storage.Models;
namespace BTCPayServer.Models.ServerViewModels
{
public class ViewFilesViewModel
{
public List<StoredFile> Files { get; set; }
public string DirectFileUrl { get; set; }
public string SelectedFileId { get; set; }
public bool StorageConfigured { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace BTCPayServer.Storage.Models
{
public enum StorageProvider
{
AzureBlobStorage=0,
// AmazonS3 =1,
// GoogleCloudStorage =2,
FileSystem =3
}
}

View File

@@ -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<JObject>(string.IsNullOrEmpty(ConfigurationStr) ? "{}" : ConfigurationStr);
set => ConfigurationStr = value.ToString();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<IStorageProviderService> _providers;
private readonly SettingsRepository _SettingsRepository;
public FileService(StoredFileRepository fileRepository, IEnumerable<IStorageProviderService> providers,
SettingsRepository settingsRepository)
{
_FileRepository = fileRepository;
_providers = providers;
_SettingsRepository = settingsRepository;
}
public async Task<StoredFile> AddFile(IFormFile file, string userId)
{
var settings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
var provider = GetProvider(settings);
var storedFile = await provider.AddFile(file, settings);
storedFile.ApplicationUserId = userId;
await _FileRepository.AddFile(storedFile);
return storedFile;
}
public async Task<string> GetFileUrl(string fileId)
{
var settings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
var provider = GetProvider(settings);
var storedFile = await _FileRepository.GetFile(fileId);
return storedFile == null ? null: await provider.GetFileUrl(storedFile, settings);
}
public async Task<string> GetTemporaryFileUrl(string fileId, DateTimeOffset expiry, bool isDownload)
{
var settings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
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<StorageSettings>();
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));
}
}
}

View File

@@ -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<AmazonS3StorageConfiguration>
// {
// public override StorageProvider StorageProvider()
// {
// return Storage.Models.StorageProvider.AmazonS3;
// }
//
// public override AmazonS3StorageConfiguration GetProviderConfiguration(StorageSettings configuration)
// {
// return configuration.Configuration.ParseAmazonS3StorageConfiguration();
// }
//
// protected override Task<IStorageProvider> GetStorageProvider(AmazonS3StorageConfiguration configuration)
// {
// return Task.FromResult<IStorageProvider>(new AmazonStorageProvider(configuration));
// }
// }
//}

View File

@@ -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<AmazonS3StorageConfiguration>();
}
public static JObject ConvertConfiguration(this AmazonS3StorageConfiguration configuration)
{
return JObject.FromObject(configuration);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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<IStorageProvider> GetStorageProvider(AzureBlobStorageConfiguration configuration)
{
return Task.FromResult<IStorageProvider>(new AzureStorageProvider(configuration));
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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<AzureBlobStorageConfiguration>();
}
public static JObject ConvertConfiguration(this AzureBlobStorageConfiguration configuration)
{
return JObject.FromObject(configuration);
}
}
}

View File

@@ -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<TStorageConfiguration> : IStorageProviderService
where TStorageConfiguration : IBaseStorageConfiguration
{
public abstract StorageProvider StorageProvider();
public virtual async Task<StoredFile> 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<string> 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<string> 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<IStorageProvider> GetStorageProvider(TStorageConfiguration configuration);
}
}

View File

@@ -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<FileSystemStorageConfiguration>();
}
public static JObject ConvertConfiguration(this FileSystemStorageConfiguration configuration)
{
return JObject.FromObject(configuration);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<FileSystemStorageConfiguration>
{
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<IStorageProvider> GetStorageProvider(FileSystemStorageConfiguration configuration)
{
return Task.FromResult<IStorageProvider>(
new LocalStorageProvider(new DirectoryInfo(GetStorageDir(_Options)).FullName));
}
public override async Task<string> 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<string> GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, DateTimeOffset expiry, bool isDownload,
BlobUrlAccess access = BlobUrlAccess.Read)
{
return GetFileUrl(storedFile, configuration);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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<GoogleCloudStorageConfiguration>();
}
public static JObject ConvertConfiguration(this GoogleCloudStorageConfiguration configuration)
{
return JObject.FromObject(configuration);
}
}
}

View File

@@ -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<IStorageProvider> GetStorageProvider(
// GoogleCloudStorageConfiguration configuration)
// {
// return Task.FromResult<IStorageProvider>(new GoogleStorageProvider(GoogleCredential.FromJson(configuration.JsonCredentials), configuration));
// }
// }
//}

View File

@@ -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<StoredFile> AddFile(IFormFile formFile, StorageSettings configuration);
Task RemoveFile(StoredFile storedFile, StorageSettings configuration);
Task<string> GetFileUrl(StoredFile storedFile, StorageSettings configuration);
Task<string> GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration,
DateTimeOffset expiry, bool isDownload, BlobUrlAccess access = BlobUrlAccess.Read);
StorageProvider StorageProvider();
}
}

View File

@@ -0,0 +1,7 @@
namespace BTCPayServer.Storage.Services.Providers.Models
{
public interface IBaseStorageConfiguration
{
string ContainerName { get; set; }
}
}

View File

@@ -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<StoredFile> GetFile(string fileId)
{
var filesResult = await GetFiles(new FilesQuery() {Id = new string[] {fileId}});
return filesResult.FirstOrDefault();
}
public async Task<List<StoredFile>> 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<string>();
public string[] UserIds { get; set; } = Array.Empty<string>();
}
}
}

View File

@@ -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<StoredFileRepository>();
serviceCollection.AddSingleton<FileService>();
// serviceCollection.AddSingleton<IStorageProviderService, AmazonS3FileProviderService>();
serviceCollection.AddSingleton<IStorageProviderService, AzureBlobStorageFileProviderService>();
serviceCollection.AddSingleton<IStorageProviderService, FileSystemFileProviderService>();
// serviceCollection.AddSingleton<IStorageProviderService, GoogleCloudStorageFileProviderService>();
}
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)
});
}
}
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,44 @@
@using BTCPayServer.Controllers
@model BTCPayServer.Controllers.ServerController.CreateTemporaryFileUrlViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Create temporary file link");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form method="post">
<div class="form-group">
<label asp-for="IsDownload"></label>
<input type="checkbox" class="form-check" asp-for="IsDownload"/>
<span asp-validation-for="IsDownload" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TimeAmount" class="control-label"></label>
<div class="input-group">
<input type="number" asp-for="TimeAmount" class="form-control">
<div class="input-group-append">
<select asp-for="TimeType" asp-items="@Html.GetEnumSelectList< ServerController.CreateTemporaryFileUrlViewModel.TmpFileTimeType>()" class="custom-select"></select>
</div>
</div>
<span asp-validation-for="TimeAmount" class="text-danger"></span>
<span asp-validation-for="TimeType" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Generate">Generate</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -0,0 +1,61 @@
@model BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration.AmazonS3StorageConfiguration
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Amazon S3");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form method="post">
<div class="form-group">
<label asp-for="ContainerName"></label>
<input class="form-control" asp-for="ContainerName" />
<span asp-validation-for="ContainerName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SecretKey"></label>
<input class="form-control" asp-for="SecretKey"/>
<span asp-validation-for="SecretKey" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Bucket"></label>
<input class="form-control" asp-for="Bucket"/>
<span asp-validation-for="Bucket" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ServiceUrl"></label>
<input class="form-control" asp-for="ServiceUrl"/>
<span asp-validation-for="ServiceUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ServerSideEncryptionMethod"></label>
<input class="form-control" asp-for="ServerSideEncryptionMethod"/>
<span asp-validation-for="ServerSideEncryptionMethod" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProfileName"></label>
<input class="form-control" asp-for="ProfileName"/>
<span asp-validation-for="ProfileName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ChunkedUploadThreshold"></label>
<input class="form-control" type="number" asp-for="ChunkedUploadThreshold"/>
<span asp-validation-for="ChunkedUploadThreshold" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<a asp-action="Storage" asp-route-forceChoice="true" >Change Storage provider</a>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -0,0 +1,36 @@
@model BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration.AzureBlobStorageConfiguration
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Azure Blob Storage");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form method="post">
<div class="form-group">
<label asp-for="ContainerName"></label>
<input class="form-control" asp-for="ContainerName" />
<span asp-validation-for="ContainerName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ConnectionString"></label>
<input class="form-control" type="text" asp-for="ConnectionString"/>
<span asp-validation-for="ConnectionString" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<a asp-action="Storage" asp-route-forceChoice="true" >Change Storage provider</a>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -0,0 +1,26 @@
@model BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration.FileSystemStorageConfiguration
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Local Filesystem");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<p>Nothing to configure here. Data will be saved in the btcpay data directory.</p>
<div class="row">
<div class="col-lg-6">
<form method="post">
<button type="submit" class="btn btn-primary" name="command" value="Save">Choose</button>
<a asp-action="Storage" asp-route-forceChoice="true" >Change Storage provider</a>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -0,0 +1,45 @@
@model BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration.GoogleCloudStorageConfiguration
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Google Cloud Storage");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form method="post">
<div class="form-group">
<label asp-for="ContainerName"></label>
<input class="form-control" asp-for="ContainerName" />
<span asp-validation-for="ContainerName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="JsonCredentials"></label>
<input class="form-control" asp-for="JsonCredentials"/>
<span asp-validation-for="JsonCredentials" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Bucket"></label>
<input class="form-control" asp-for="Bucket"/>
<span asp-validation-for="Bucket" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email"></label>
<input class="form-control" asp-for="Email"/>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<a asp-action="Storage" asp-route-forceChoice="true" >Change Storage provider</a>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -0,0 +1,112 @@
@model ViewFilesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Files);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>User</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var file in Model.Files)
{
<tr>
<td>@file.FileName</td>
<td>@file.Timestamp.ToString("g")</td>
<td>@file.ApplicationUser.UserName</td>
<td>
<a asp-action="Files" asp-route-fileId="@file.Id">Get Link</a>
- <a asp-action="CreateTemporaryFileUrl" asp-route-fileId="@file.Id">Get Temp Link</a>
- <a asp-action="DeleteFile" asp-route-fileId="@file.Id">Remove</a>
</td>
</tr>
}
@if (!Model.Files.Any())
{
<tr>
<td colspan="4" class="text-center">No files</td>
</tr>
}
</tbody>
</table>
@if (!string.IsNullOrEmpty(Model.SelectedFileId))
{
var file = Model.Files.Single(storedFile => storedFile.Id.Equals(Model.SelectedFileId, StringComparison.InvariantCultureIgnoreCase));
<div class="card mb-2">
<div class="card-text">
<ul class="list-group list-group-flush">
<li class="list-group-item">
@file.FileName
</li>
<li class="list-group-item">
<div class="row px-0 mx-0">
<span class="col-sm-12 col-md-2">Url: </span>
<a class="col-sm-12 col-md-10 text-right" asp-action="GetFile" asp-controller="Storage" asp-route-fileId="@Model.SelectedFileId" target="_blank">
@Url.Action("GetFile", "Storage", new
{
fileId = Model.SelectedFileId
}, Context.Request.Scheme, Context.Request.Host.ToString(), Context.Request.PathBase.ToString())
</a>
</div>
</li>
<li class="list-group-item ">
<div class="row px-0 mx-0">
<span class="col-sm-12 col-md-2">Direct Url </span>
<a class="col-sm-12 col-md-10 text-right" href="@Model.DirectFileUrl" target="_blank">@Model.DirectFileUrl</a>
</div>
</li>
</ul>
</div>
</div>
}
@if (Model.StorageConfigured)
{
<div class="card">
<form asp-action="CreateFile" method="post" enctype="multipart/form-data">
<div class="card-body">
<h3 class="header">Upload File</h3>
<div class="custom-file">
<input type="file" class="custom-file-input" name="file" id="file" required>
<label class="custom-file-label" for="customFile">Choose file</label>
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Upload file</button>
</div>
</form>
</div>
@section Scripts {
<script>
$(document).ready(function() {
$('.custom-file-input').on('change',
function() {
var label = $(this).next('label');
if (document.getElementById("file").files.length > 0) {
var fileName = document.getElementById("file").files[0].name;
label.addClass("selected").html(fileName);
} else {
label.removeClass("selected").html("Choose file");
}
});
});
</script>
}
}

View File

@@ -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
}
}

View File

@@ -5,7 +5,7 @@
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]"/>
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
@@ -26,7 +26,7 @@
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
<th style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
@@ -35,7 +35,7 @@
<tr>
<td>@s.CryptoCode</td>
<td>@s.DisplayName</td>
<td style="text-align:right">
<td style="text-align: right">
<a asp-action="Service" asp-route-serviceName="@s.ServiceName" asp-route-cryptoCode="@s.CryptoCode">See information</a>
</td>
</tr>
@@ -59,7 +59,7 @@
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
<th style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
@@ -67,7 +67,7 @@
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">
<td style="text-align: right">
<a href="@s.Link" target="_blank">See information</a>
</td>
</tr>
@@ -92,7 +92,7 @@
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
<th style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
@@ -100,7 +100,7 @@
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">
<td style="text-align: right">
<a href="@s.Link" target="_blank">See information</a>
</td>
</tr>
@@ -125,7 +125,7 @@
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
<th style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
@@ -133,7 +133,7 @@
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">@s.Link</td>
<td style="text-align: right">@s.Link</td>
</tr>
}
</tbody>
@@ -142,6 +142,35 @@
</div>
</div>
}
<div class="row">
<div class="col-md-8">
<h4>External storage services</h4>
<div class="form-group">
<span>Integrated Storage providers to store file uploads from btcpay</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var s in Model.ExternalStorageServices)
{
<tr>
<td>@s.Name</td>
<td style="text-align: right">
<a href="@s.Link">Edit</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")

View File

@@ -0,0 +1,36 @@
@using BTCPayServer.Storage.Models
@model BTCPayServer.Storage.ViewModels.ChooseStorageViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
@if (Model.ShowChangeWarning)
{
<div class="alert alert-danger">
If you change your configured storage provider, your current files will become inaccessible.
</div>
}
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form method="post">
<div class="form-group">
<label asp-for="Provider"></label>
<select asp-for="Provider" asp-items="@Html.GetEnumSelectList<StorageProvider>()" class="form-control"></select>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Next</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -7,5 +7,6 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="Logs">Logs</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
</div>