Skip to content

Commit e6956c2

Browse files
committed
Initial Version of Azure Upload
1 parent f07c9a4 commit e6956c2

15 files changed

+281
-1
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0.97167" PrivateAssets="All" IncludeAssets="Runtime;Build;Native;contentFiles;Analyzers" />
88
</ItemGroup>
99
<ItemGroup Label="Infrastructure">
10+
<PackageVersion Include="Azure.Storage.Blobs" Version="12.22.2" />
1011
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.0-rc.2.24474.1" />
1112
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-rc.2.24474.1" />
1213
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-rc.2.24474.1" />

src/LinkDotNet.Blog.Web/ConfigurationExtension.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
22
using LinkDotNet.Blog.Domain;
33
using LinkDotNet.Blog.Web.Authentication.OpenIdConnect;
4+
using LinkDotNet.Blog.Web.Features.Services.FileUpload;
45
using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components;
56
using LinkDotNet.Blog.Web.Features.SupportMe.Components;
67
using Microsoft.Extensions.Configuration;
78
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Options;
810

911
namespace LinkDotNet.Blog.Web;
1012

@@ -20,7 +22,8 @@ public static IServiceCollection AddConfiguration(this IServiceCollection servic
2022
.AddProfileInformationConfigurations()
2123
.AddGiscusConfiguration()
2224
.AddDisqusConfiguration()
23-
.AddSupportMeConfiguration();
25+
.AddSupportMeConfiguration()
26+
.AddImageUploadConfiguration();
2427

2528
return services;
2629
}
@@ -129,4 +132,17 @@ private static IServiceCollection AddSupportMeConfiguration(this IServiceCollect
129132
});
130133
return services;
131134
}
135+
136+
private static IServiceCollection AddImageUploadConfiguration(this IServiceCollection services)
137+
{
138+
ArgumentNullException.ThrowIfNull(services);
139+
140+
services.AddOptions<UploadConfiguration>()
141+
.Configure<IConfiguration>((settings, config) =>
142+
{
143+
config.GetSection(UploadConfiguration.ConfigurationSection)
144+
.Bind(settings);
145+
});
146+
return services;
147+
}
132148
}

src/LinkDotNet.Blog.Web/Features/Components/MarkdownTextArea.razor

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
@using System.IO
2+
@using Blazorise
13
@using Blazorise.Markdown
4+
@using LinkDotNet.Blog.Web.Features.Services.FileUpload
5+
@using IToastService = Blazored.Toast.Services.IToastService
26
@inject IJSRuntime JsRuntime
7+
@inject IBlobUploadService BlobUploadService
8+
@inject IToastService ToastService
39

410
<Markdown ElementId="@Id"
511
Placeholder="@Placeholder"
@@ -8,13 +14,19 @@
814
ValueChanged="s => Value = s"
915
MaxHeight="@Height"
1016
PreviewRender="PreviewFunction"
17+
ImageAccept="image/*"
18+
ImageUploadChanged="UploadFiles"
1119
PreviewImagesInEditor="true"/>
1220

21+
<UploadFileModalDialog @ref="UploadDialog"></UploadFileModalDialog>
22+
1323
@code {
1424
private string textContent = string.Empty;
1525

1626
private string Height => $"{Rows * 25}px";
1727

28+
private UploadFileModalDialog UploadDialog { get; set; } = default!;
29+
1830
#pragma warning disable BL0007
1931
[Parameter]
2032
public string Value
@@ -45,4 +57,31 @@
4557

4658
[Parameter] public Func<string, Task<string>> PreviewFunction { get; set; } =
4759
s => Task.FromResult(MarkdownConverter.ToMarkupString(s).Value);
60+
61+
private async Task UploadFiles(FileChangedEventArgs arg)
62+
{
63+
try
64+
{
65+
foreach (var file in arg.Files)
66+
{
67+
using var memoryStream = new MemoryStream();
68+
await file.WriteToStreamAsync(memoryStream);
69+
memoryStream.Position = 0;
70+
var options = await UploadDialog.ShowAsync(file.Name);
71+
if (options is null)
72+
{
73+
continue;
74+
}
75+
76+
var url = await BlobUploadService.UploadFileAsync(options.Name, memoryStream, new());
77+
file.UploadUrl = url;
78+
ToastService.ShowSuccess($"Successfully uploaded {file.Name}");
79+
}
80+
}
81+
catch (Exception e)
82+
{
83+
ToastService.ShowError($"Error while uploading file: {e.Message}");
84+
}
85+
}
86+
4887
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<div class="modal @modalClass" tabindex="-1" role="dialog" style="display:@modalDisplay; overflow-y: auto;">
2+
<div class="modal-dialog modal-lg" role="document">
3+
<div class="modal-content">
4+
<div class="modal-header">
5+
<h5 class="modal-title">Configuration</h5>
6+
<button type="button" class="btn-close" @onclick="OnAbort"></button>
7+
</div>
8+
<div class="modal-body">
9+
<EditForm Model="@model" OnValidSubmit="OnOk">
10+
<DataAnnotationsValidator />
11+
<div class="form-floating mb-3">
12+
<InputText type="text" class="form-control" id="name" placeholder="Filename"
13+
@bind-Value="model.Name" />
14+
<label for="name">Filename</label>
15+
<ValidationMessage For="() => model.Name"></ValidationMessage>
16+
</div>
17+
<div class="form-check form-switch mb-3">
18+
<InputCheckbox class="form-check-input" id="cache" @bind-Value="model.CacheMedia" />
19+
<label class="form-check-label" for="cache">Enable Media Caching</label><br />
20+
<small class="form-text text-body-secondary">If enabled, the browser will cache the media file</small>
21+
</div>
22+
</EditForm>
23+
</div>
24+
<div class="modal-footer">
25+
<button type="button" class="btn btn-secondary" @onclick="OnAbort">Abort</button>
26+
<button type="button" class="btn btn-primary" @onclick="OnOk">OK</button>
27+
</div>
28+
</div>
29+
</div>
30+
</div>
31+
32+
@if (showBackdrop)
33+
{
34+
<div class="modal-backdrop fade show"></div>
35+
}
36+
37+
@code {
38+
private TaskCompletionSource<UploadFileModalDialogObject?> result = null!;
39+
private string modalDisplay = "none;";
40+
private string modalClass = "";
41+
private bool showBackdrop;
42+
private readonly UploadFileModalDialogObject model = new();
43+
44+
public async Task<UploadFileModalDialogObject?> ShowAsync(string fileName)
45+
{
46+
modalDisplay = "block;";
47+
modalClass = "show";
48+
showBackdrop = true;
49+
model.Name = fileName;
50+
result = new TaskCompletionSource<UploadFileModalDialogObject?>();
51+
StateHasChanged();
52+
return await result.Task;
53+
}
54+
55+
private void OnAbort()
56+
{
57+
modalDisplay = "none";
58+
modalClass = "";
59+
showBackdrop = false;
60+
result.SetResult(null);
61+
StateHasChanged();
62+
}
63+
64+
private void OnOk()
65+
{
66+
modalDisplay = "none";
67+
modalClass = "";
68+
showBackdrop = false;
69+
result.SetResult(model);
70+
StateHasChanged();
71+
}
72+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace LinkDotNet.Blog.Web.Features.Components;
4+
5+
public class UploadFileModalDialogObject
6+
{
7+
[Required]
8+
public string Name { get; set; }
9+
10+
public bool CacheMedia { get; set; } = true;
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using LinkDotNet.Blog.Domain;
2+
3+
namespace LinkDotNet.Blog.Web.Features.Services.FileUpload;
4+
5+
public class AuthenticationMode : Enumeration<AuthenticationMode>
6+
{
7+
public static readonly AuthenticationMode Default = new(nameof(Default));
8+
9+
public static readonly AuthenticationMode ConnectionString = new(nameof(ConnectionString));
10+
11+
public AuthenticationMode(string key) : base(key)
12+
{
13+
}
14+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using System.Web;
5+
using Azure.Identity;
6+
using Azure.Storage.Blobs;
7+
using Azure.Storage.Blobs.Models;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace LinkDotNet.Blog.Web.Features.Services.FileUpload;
11+
12+
public class AzureBlobStorageService : IBlobUploadService
13+
{
14+
private readonly IOptions<UploadConfiguration> azureBlobStorageConfiguration;
15+
16+
public AzureBlobStorageService(IOptions<UploadConfiguration> azureBlobStorageConfiguration)
17+
{
18+
this.azureBlobStorageConfiguration = azureBlobStorageConfiguration;
19+
}
20+
21+
public async Task<string> UploadFileAsync(string fileName, Stream fileStream, UploadOptions options)
22+
{
23+
var containerName = azureBlobStorageConfiguration.Value.ContainerName;
24+
var client = CreateClient(azureBlobStorageConfiguration.Value);
25+
var blobContainerClient = client.GetBlobContainerClient(containerName);
26+
var blobClient = blobContainerClient.GetBlobClient(fileName);
27+
28+
var blobOptions = new BlobUploadOptions();
29+
if (options.SetCacheControlHeader)
30+
{
31+
blobOptions.HttpHeaders = new BlobHttpHeaders
32+
{
33+
CacheControl = "public, max-age=31536000"
34+
};
35+
}
36+
37+
await blobClient.UploadAsync(fileStream, blobOptions);
38+
return blobClient.Uri.AbsoluteUri;
39+
}
40+
41+
private static BlobServiceClient CreateClient(UploadConfiguration configuration)
42+
{
43+
if (configuration.AuthenticationMode == AuthenticationMode.ConnectionString.Key)
44+
{
45+
var connectionString = configuration.ConnectionString
46+
?? throw new InvalidOperationException("ConnectionString must be set when using ConnectionString authentication mode");
47+
return new BlobServiceClient(connectionString);
48+
}
49+
50+
var serviceUrl = configuration.ServiceUrl
51+
?? throw new InvalidOperationException("ServiceUrl must be set when using Default authentication mode");
52+
53+
return new BlobServiceClient(new Uri(serviceUrl), new DefaultAzureCredential());
54+
}
55+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.IO;
2+
using System.Threading.Tasks;
3+
4+
namespace LinkDotNet.Blog.Web.Features.Services.FileUpload;
5+
6+
public interface IBlobUploadService
7+
{
8+
public Task<string> UploadFileAsync(string fileName, Stream fileStream, UploadOptions options);
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.IO;
2+
using System.Threading.Tasks;
3+
4+
namespace LinkDotNet.Blog.Web.Features.Services.FileUpload;
5+
6+
public class NoopStorageService : IBlobUploadService
7+
{
8+
public Task<string> UploadFileAsync(string fileName, Stream fileStream, UploadOptions options)
9+
{
10+
return Task.FromResult("No Storage Service was configured. Nothing was uploaded");
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace LinkDotNet.Blog.Web.Features.Services.FileUpload;
2+
3+
public class UploadConfiguration
4+
{
5+
internal const string ConfigurationSection = "ImageStorage";
6+
7+
public required string AuthenticationMode { get; init; }
8+
public string? ServiceUrl { get; init; }
9+
public string? ConnectionString { get; init; }
10+
public required string ContainerName { get; init; }
11+
}

0 commit comments

Comments
 (0)