Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Extensions" Version="4.3.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
Expand All @@ -42,7 +47,6 @@
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.5" />
Expand Down Expand Up @@ -70,7 +74,6 @@
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.5" />
Expand Down
54 changes: 54 additions & 0 deletions WOPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost.AppHost", "infra\W
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost.ServiceDefaults", "infra\WopiHost.ServiceDefaults\WopiHost.ServiceDefaults.csproj", "{8EF68B93-D00E-440D-9267-277FC39961A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.AzureStorageProvider", "src\WopiHost.AzureStorageProvider\WopiHost.AzureStorageProvider.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost.AzureStorageProvider.Tests", "test\WopiHost.AzureStorageProvider.Tests\WopiHost.AzureStorageProvider.Tests.csproj", "{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost.AzureStorageProvider.Sample", "sample\WopiHost.AzureStorageProvider\WopiHost.AzureStorageProvider.Sample.csproj", "{67496D2A-8F52-4C50-AD41-AFC90767172C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -371,6 +377,54 @@ Global
{8EF68B93-D00E-440D-9267-277FC39961A5}.Release|x64.Build.0 = Release|Any CPU
{8EF68B93-D00E-440D-9267-277FC39961A5}.Release|x86.ActiveCfg = Release|Any CPU
{8EF68B93-D00E-440D-9267-277FC39961A5}.Release|x86.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|x64.ActiveCfg = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|x64.Build.0 = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|x86.ActiveCfg = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Debug|x86.Build.0 = Debug|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|Any CPU.Build.0 = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|x64.ActiveCfg = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|x64.Build.0 = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|x86.ActiveCfg = Release|Any CPU
{F5EF51B5-F8D8-4DF4-89EC-55ECBDA7AC12}.Release|x86.Build.0 = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|x64.ActiveCfg = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|x64.Build.0 = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|x86.ActiveCfg = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Debug|x86.Build.0 = Debug|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|Any CPU.Build.0 = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|x64.ActiveCfg = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|x64.Build.0 = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|x86.ActiveCfg = Release|Any CPU
{67496D2A-8F52-4C50-AD41-AFC90767172C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
40 changes: 40 additions & 0 deletions sample/WopiHost.AzureStorageProvider/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using WopiHost.AzureStorageProvider;
using WopiHost.Core.Extensions;
using WopiHost.Abstractions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

// Configure Azure Storage provider
builder.Services.Configure<WopiAzureStorageProviderOptions>(
builder.Configuration.GetSection("WopiHost:StorageOptions"));

// Register Azure Storage services
builder.Services.AddSingleton<AzureFileIds>();
builder.Services.AddScoped<IWopiStorageProvider, WopiAzureStorageProvider>();
builder.Services.AddScoped<IWopiWritableStorageProvider, WopiAzureStorageProvider>();
builder.Services.AddScoped<IWopiSecurityHandler, WopiAzureSecurityHandler>();

// Add WOPI services
builder.Services.AddWopi();

// Add OpenAPI services
builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
// OpenAPI endpoint will be available at /openapi/v1.json
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"WopiHost.AzureStorageProvider.Sample": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:3702;http://localhost:3703"
}
}
}
106 changes: 106 additions & 0 deletions sample/WopiHost.AzureStorageProvider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# WopiHost.AzureStorageProvider Sample

This sample demonstrates how to use the WopiHost.AzureStorageProvider with Azure Blob Storage.

## Prerequisites

1. An Azure Storage Account
2. .NET 8.0 or later
3. Visual Studio 2022 or VS Code

## Configuration

1. Update the `appsettings.json` file with your Azure Storage connection details:

```json
{
"WopiHost": {
"StorageOptions": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=yourkey;EndpointSuffix=core.windows.net",
"ContainerName": "wopi-files",
"RootPath": "documents",
"UseManagedIdentity": false,
"CreateContainerIfNotExists": true
},
"ClientUrl": "https://your-office-online-server.com/hosting/discovery"
}
}
```

## Running the Sample

1. Navigate to the sample directory:
```bash
cd sample/WopiHost.AzureStorageProvider
```

2. Restore packages:
```bash
dotnet restore
```

3. Run the application:
```bash
dotnet run
```

4. Open your browser and navigate to `https://localhost:5001` (or the URL shown in the console)
5. The OpenAPI specification will be available at `/openapi/v1.json`

## Features Demonstrated

- Azure Blob Storage integration
- File upload/download operations
- Container management
- Security handling
- WOPI protocol implementation

## Authentication Options

The sample supports multiple authentication methods:

### Connection String
```json
{
"StorageOptions": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=...",
"ContainerName": "wopi-files"
}
}
```

### Account Key
```json
{
"StorageOptions": {
"AccountName": "yourstorageaccount",
"AccountKey": "yourkey",
"ContainerName": "wopi-files"
}
}
```

### Managed Identity
```json
{
"StorageOptions": {
"AccountName": "yourstorageaccount",
"UseManagedIdentity": true,
"ContainerName": "wopi-files"
}
}
```

## Testing

You can test the WOPI endpoints using tools like Postman or curl:

- Discovery: `GET /wopi/discovery`
- CheckFileInfo: `GET /wopi/files/{fileId}`
- GetFile: `GET /wopi/files/{fileId}/contents`

## Troubleshooting

1. **Container not found**: Ensure the container exists or set `CreateContainerIfNotExists` to `true`
2. **Authentication failed**: Verify your connection string or account credentials
3. **Permission denied**: Check that your Azure Storage account has the necessary permissions
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>WopiHost.AzureStorageProvider.Sample</AssemblyName>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\WopiHost.AzureStorageProvider\WopiHost.AzureStorageProvider.csproj" />
<ProjectReference Include="..\..\src\WopiHost.Core\WopiHost.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions sample/WopiHost.AzureStorageProvider/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"WopiHost": {
"StorageOptions": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=yourkey;EndpointSuffix=core.windows.net",
"ContainerName": "wopi-files",
"RootPath": "documents",
"UseManagedIdentity": false,
"CreateContainerIfNotExists": true,
"FileNameMaxLength": 250
},
"ClientUrl": "https://your-office-online-server.com/hosting/discovery"
}
}
84 changes: 84 additions & 0 deletions src/WopiHost.AzureStorageProvider/AzureBlobWriteStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

namespace WopiHost.AzureStorageProvider;

/// <summary>
/// A write stream that uploads to Azure Blob Storage when disposed.
/// </summary>
internal class AzureBlobWriteStream(BlobClient blobClient, CancellationToken cancellationToken) : Stream
{
private readonly MemoryStream _memoryStream = new();
private bool _disposed = false;

public override bool CanRead => _memoryStream.CanRead;
public override bool CanSeek => _memoryStream.CanSeek;
public override bool CanWrite => _memoryStream.CanWrite;
public override long Length => _memoryStream.Length;
public override long Position
{
get => _memoryStream.Position;
set => _memoryStream.Position = value;
}

public override void Flush() => _memoryStream.Flush();

public override int Read(byte[] buffer, int offset, int count) => _memoryStream.Read(buffer, offset, count);

public override long Seek(long offset, SeekOrigin origin) => _memoryStream.Seek(offset, origin);

public override void SetLength(long value) => _memoryStream.SetLength(value);

public override void Write(byte[] buffer, int offset, int count) => _memoryStream.Write(buffer, offset, count);

public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
await _memoryStream.WriteAsync(buffer, offset, count, cancellationToken);
}

protected override void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
try
{
_memoryStream.Position = 0;
var uploadOptions = new BlobUploadOptions();
blobClient.UploadAsync(_memoryStream, uploadOptions, cancellationToken).Wait();
}
catch
{
// Log error but don't throw during disposal
}
finally
{
_memoryStream.Dispose();
_disposed = true;
}
}
base.Dispose(disposing);
}

public override async ValueTask DisposeAsync()
{
if (!_disposed)
{
try
{
_memoryStream.Position = 0;
var uploadOptions = new BlobUploadOptions();
await blobClient.UploadAsync(_memoryStream, uploadOptions, cancellationToken);
}
catch
{
// Log error but don't throw during disposal
}
finally
{
await _memoryStream.DisposeAsync();
_disposed = true;
}
}
await base.DisposeAsync();
}
}
Loading