Skip to content

Commit b3990e5

Browse files
Add IFileProvider implementation
1 parent a33c198 commit b3990e5

File tree

4 files changed

+286
-0
lines changed

4 files changed

+286
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Azure.Storage.Blobs;
6+
using Azure.Storage.Blobs.Models;
7+
using Microsoft.Extensions.FileProviders;
8+
9+
namespace Umbraco.StorageProviders.AzureBlob
10+
{
11+
/// <summary>
12+
/// Represents a virtual hierarchy of Azure Blob Storage blobs.
13+
/// </summary>
14+
/// <seealso cref="Microsoft.Extensions.FileProviders.IDirectoryContents" />
15+
public class AzureBlobDirectoryContents : IDirectoryContents
16+
{
17+
private readonly BlobContainerClient _containerClient;
18+
private readonly IList<BlobHierarchyItem> _items;
19+
20+
/// <inheritdoc />
21+
public bool Exists { get; }
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="AzureBlobDirectoryContents" /> class.
25+
/// </summary>
26+
/// <param name="containerClient">The container client.</param>
27+
/// <param name="items">The items.</param>
28+
/// <exception cref="System.ArgumentNullException">containerClient
29+
/// or
30+
/// items</exception>
31+
public AzureBlobDirectoryContents(BlobContainerClient containerClient, IList<BlobHierarchyItem> items)
32+
{
33+
_containerClient = containerClient ?? throw new ArgumentNullException(nameof(containerClient));
34+
_items = items ?? throw new ArgumentNullException(nameof(items));
35+
36+
Exists = _items.Count > 0;
37+
}
38+
39+
/// <inheritdoc />
40+
public IEnumerator<IFileInfo> GetEnumerator()
41+
=> _items.Select<BlobHierarchyItem, IFileInfo>(x => x.IsPrefix
42+
? new AzureBlobPrefixInfo(x.Prefix)
43+
: new AzureBlobItemInfo(_containerClient.GetBlobClient(x.Blob.Name), x.Blob.Properties)).GetEnumerator();
44+
45+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
46+
}
47+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net;
4+
using Azure;
5+
using Azure.Storage.Blobs;
6+
using Azure.Storage.Blobs.Models;
7+
using Microsoft.Extensions.FileProviders;
8+
using Microsoft.Extensions.Primitives;
9+
using Umbraco.Cms.Core;
10+
using Umbraco.Extensions;
11+
using Umbraco.StorageProviders.AzureBlob.IO;
12+
13+
namespace Umbraco.StorageProviders.AzureBlob
14+
{
15+
/// <summary>
16+
/// Represents a read-only Azure Blob Storage file provider.
17+
/// </summary>
18+
/// <seealso cref="Microsoft.Extensions.FileProviders.IFileProvider" />
19+
public class AzureBlobFileProvider : IFileProvider
20+
{
21+
private readonly BlobContainerClient _containerClient;
22+
private readonly string? _containerRootPath;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="AzureBlobFileProvider" /> class.
26+
/// </summary>
27+
/// <param name="containerClient">The container client.</param>
28+
/// <param name="containerRootPath">The container root path.</param>
29+
/// <exception cref="System.ArgumentNullException">containerClient</exception>
30+
public AzureBlobFileProvider(BlobContainerClient containerClient, string? containerRootPath = null)
31+
{
32+
_containerClient = containerClient ?? throw new ArgumentNullException(nameof(containerClient));
33+
_containerRootPath = containerRootPath?.Trim(Constants.CharArrays.ForwardSlash);
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="AzureBlobFileProvider" /> class.
38+
/// </summary>
39+
/// <param name="options">The options.</param>
40+
/// <exception cref="System.ArgumentNullException">options</exception>
41+
public AzureBlobFileProvider(AzureBlobFileSystemOptions options)
42+
{
43+
if (options is null)
44+
{
45+
throw new ArgumentNullException(nameof(options));
46+
}
47+
48+
_containerClient = new BlobContainerClient(options.ConnectionString, options.ContainerName);
49+
_containerRootPath = options.ContainerRootPath?.Trim(Constants.CharArrays.ForwardSlash);
50+
}
51+
52+
/// <inheritdoc />
53+
public IDirectoryContents GetDirectoryContents(string subpath)
54+
{
55+
var path = GetFullPath(subpath);
56+
57+
// Get all blobs and iterate to fetch all pages
58+
var blobs = new List<BlobHierarchyItem>();
59+
foreach (var item in _containerClient.GetBlobsByHierarchy(delimiter: "/", prefix: path))
60+
{
61+
blobs.Add(item);
62+
}
63+
64+
if (blobs.Count == 0)
65+
{
66+
return NotFoundDirectoryContents.Singleton;
67+
}
68+
69+
return new AzureBlobDirectoryContents(_containerClient, blobs);
70+
}
71+
72+
/// <inheritdoc />
73+
public IFileInfo GetFileInfo(string subpath)
74+
{
75+
var path = GetFullPath(subpath);
76+
var blobClient = _containerClient.GetBlobClient(path);
77+
78+
BlobProperties properties;
79+
try
80+
{
81+
properties = blobClient.GetProperties().Value;
82+
}
83+
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.NotFound)
84+
{
85+
return new NotFoundFileInfo(AzureBlobItemInfo.ParseName(path));
86+
}
87+
88+
return new AzureBlobItemInfo(blobClient, properties);
89+
}
90+
91+
/// <inheritdoc />
92+
public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
93+
94+
private string GetFullPath(string subpath) => _containerRootPath + subpath.EnsureStartsWith('/');
95+
}
96+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.IO;
3+
using Azure.Storage.Blobs;
4+
using Azure.Storage.Blobs.Models;
5+
using Microsoft.Extensions.FileProviders;
6+
7+
namespace Umbraco.StorageProviders.AzureBlob
8+
{
9+
/// <summary>
10+
/// Represents an Azure Blob Storage blob item.
11+
/// </summary>
12+
/// <seealso cref="Microsoft.Extensions.FileProviders.IFileInfo" />
13+
public class AzureBlobItemInfo : IFileInfo
14+
{
15+
private readonly BlobClient _blobClient;
16+
17+
/// <inheritdoc />
18+
public bool Exists => true;
19+
20+
/// <inheritdoc />
21+
public bool IsDirectory => false;
22+
23+
/// <inheritdoc />
24+
public DateTimeOffset LastModified { get; }
25+
26+
/// <inheritdoc />
27+
public long Length { get; }
28+
29+
/// <inheritdoc />
30+
public string Name { get; }
31+
32+
/// <inheritdoc />
33+
public string PhysicalPath => null!;
34+
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="AzureBlobItemInfo" /> class.
37+
/// </summary>
38+
/// <param name="blobClient">The blob client.</param>
39+
/// <exception cref="System.ArgumentNullException">blobClient</exception>
40+
protected AzureBlobItemInfo(BlobClient blobClient)
41+
{
42+
_blobClient = blobClient ?? throw new ArgumentNullException(nameof(blobClient));
43+
44+
Name = ParseName(blobClient.Name);
45+
}
46+
47+
/// <summary>
48+
/// Initializes a new instance of the <see cref="AzureBlobItemInfo" /> class.
49+
/// </summary>
50+
/// <param name="blobClient">The blob client.</param>
51+
/// <param name="properties">The properties.</param>
52+
/// <exception cref="System.ArgumentNullException">properties</exception>
53+
public AzureBlobItemInfo(BlobClient blobClient, BlobProperties properties)
54+
: this(blobClient)
55+
{
56+
if (properties == null)
57+
{
58+
throw new ArgumentNullException(nameof(properties));
59+
}
60+
61+
LastModified = properties.LastModified;
62+
Length = properties.ContentLength;
63+
}
64+
65+
/// <summary>
66+
/// Initializes a new instance of the <see cref="AzureBlobItemInfo" /> class.
67+
/// </summary>
68+
/// <param name="blobClient">The blob client.</param>
69+
/// <param name="properties">The properties.</param>
70+
/// <exception cref="System.ArgumentNullException">properties</exception>
71+
public AzureBlobItemInfo(BlobClient blobClient, BlobItemProperties properties)
72+
: this(blobClient)
73+
{
74+
if (properties == null)
75+
{
76+
throw new ArgumentNullException(nameof(properties));
77+
}
78+
79+
LastModified = properties.LastModified.GetValueOrDefault();
80+
Length = properties.ContentLength.GetValueOrDefault(-1);
81+
}
82+
83+
/// <inheritdoc />
84+
public Stream CreateReadStream() => _blobClient.OpenRead();
85+
86+
internal static string ParseName(string path) => path.Substring(path.LastIndexOf('/') + 1);
87+
}
88+
}
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 Microsoft.Extensions.FileProviders;
4+
5+
namespace Umbraco.StorageProviders.AzureBlob
6+
{
7+
/// <summary>
8+
/// Represents an Azure Blob Storage prefix.
9+
/// </summary>
10+
/// <seealso cref="Microsoft.Extensions.FileProviders.IFileInfo" />
11+
public class AzureBlobPrefixInfo : IFileInfo
12+
{
13+
/// <inheritdoc />
14+
public bool Exists => true;
15+
16+
/// <inheritdoc />
17+
public bool IsDirectory => true;
18+
19+
/// <inheritdoc />
20+
public DateTimeOffset LastModified => default;
21+
22+
/// <inheritdoc />
23+
public long Length => -1;
24+
25+
/// <inheritdoc />
26+
public string Name { get; }
27+
28+
/// <inheritdoc />
29+
public string PhysicalPath => null!;
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="AzureBlobPrefixInfo"/> class.
33+
/// </summary>
34+
/// <param name="prefix">The prefix.</param>
35+
public AzureBlobPrefixInfo(string prefix)
36+
{
37+
if (prefix == null)
38+
{
39+
throw new ArgumentNullException(nameof(prefix));
40+
}
41+
42+
Name = ParseName(prefix);
43+
}
44+
45+
/// <inheritdoc />
46+
public Stream CreateReadStream() => throw new InvalidOperationException();
47+
48+
private static string ParseName(string prefix)
49+
{
50+
var name = prefix.TrimEnd('/');
51+
52+
return name.Substring(name.LastIndexOf('/') + 1);
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)