22using Abp . Domain . Repositories ;
33using Azure . Storage . Blobs ;
44using Azure . Storage . Blobs . Models ;
5+ using Castle . Core . Logging ;
56using Microsoft . AspNetCore . Hosting ;
67using Microsoft . Extensions . Configuration ;
78using Microsoft . Extensions . Hosting ;
@@ -20,13 +21,16 @@ public class AzureStoredFileService : StoredFileServiceBase, IStoredFileService
2021 private const string ContainerName = "files" ;
2122 private readonly IocManager _iocManager ;
2223 private readonly IConfigurationRoot _configuration ;
23- private BlobContainerClient ? _blobContainerClient ;
24+ private readonly ILogger _logger ;
25+ private readonly Lazy < BlobContainerClient > _blobContainerClient ;
2426
25- public AzureStoredFileService ( IRepository < StoredFile , Guid > fileService , IRepository < StoredFileVersion , Guid > versionService , IRepository < StoredFileVersionDownload , Guid > storedFileVersionDownloadService , IocManager iocManager )
27+ public AzureStoredFileService ( IRepository < StoredFile , Guid > fileService , IRepository < StoredFileVersion , Guid > versionService , IRepository < StoredFileVersionDownload , Guid > storedFileVersionDownloadService , IocManager iocManager , ILogger logger )
2628 : base ( fileService , versionService , storedFileVersionDownloadService )
2729 {
2830 _iocManager = iocManager ;
2931 _configuration = GetConfiguration ( ) ;
32+ _blobContainerClient = new Lazy < BlobContainerClient > ( CreateBlobContainerClient ) ;
33+ _logger = logger ;
3034 }
3135
3236 private IConfigurationRoot GetConfiguration ( )
@@ -38,36 +42,76 @@ private IConfigurationRoot GetConfiguration()
3842 /// <summary>
3943 /// Returns connection string. Note: for the Azure environment - uses standard environment variable
4044 /// </summary>
41- private string GetConnectionString ( ) => _configuration . GetRequiredConnectionString ( ConnectionStringName ) ;
42-
43- private BlobContainerClient BlobContainerClient
45+ private string GetStorageValue ( )
4446 {
45- get
46- {
47- // If Container name is not passed from the configs then we use the defaults container name which is 'files'
48- var containerName = _configuration . GetSection ( CloudStorageName ) . GetValue < string > ( "ContainerName" ) ?? ContainerName ;
49-
50- if ( _blobContainerClient != null )
51- return _blobContainerClient ;
52-
53- var containerClient = new BlobContainerClient ( GetConnectionString ( ) , containerName ) ;
54- containerClient . CreateIfNotExists ( ) ;
47+ var value = _configuration . GetSection ( CloudStorageName ) . GetValue < string > ( "ConnectionString" ) ;
48+ if ( string . IsNullOrWhiteSpace ( value ) )
49+ value = _configuration . GetConnectionString ( ConnectionStringName ) ?? throw new InvalidOperationException ( "BlobStorage Connection not set." ) ;
50+ return value ;
51+ }
5552
56- // Setup the permissions on the container to be public
57- containerClient . SetAccessPolicy ( PublicAccessType . BlobContainer ) ;
53+ /// <summary>
54+ /// Creates a <see cref="BlobContainerClient"/> by auto-detecting the authentication
55+ /// method from the format of the configured storage value.
56+ /// </summary>
57+ private BlobContainerClient CreateBlobContainerClient ( )
58+ {
59+ var value = GetStorageValue ( ) ;
60+ var containerName = _configuration . GetSection ( CloudStorageName )
61+ . GetValue < string > ( "ContainerName" ) ?? ContainerName ;
5862
59- _blobContainerClient = containerClient ;
60- return _blobContainerClient ;
63+ // URL-based auth: SAS token
64+ if ( value . StartsWith ( "https://" , StringComparison . OrdinalIgnoreCase ) )
65+ {
66+ var uri = new Uri ( value ) ;
67+ bool hasSasToken = ! string . IsNullOrEmpty ( uri . Query ) ;
68+ // uri.Segments for https://account.blob.core.windows.net/mycontainer?...
69+ // is ['/', 'mycontainer'] (length 2), indicating container is in the URL path.
70+ bool hasContainerInPath = uri . Segments . Length > 1 &&
71+ ! string . IsNullOrEmpty ( uri . Segments [ 1 ] . Trim ( '/' ) ) ;
72+
73+ if ( ! hasSasToken )
74+ throw new InvalidOperationException (
75+ $ "The configured storage URL '{ uri . Host } ' has no SAS token. " +
76+ "Provide a SAS URL (https://…?sv=…&sig=…) or a classic connection string." ) ;
77+
78+ if ( hasContainerInPath )
79+ {
80+ // Container-level SAS URL — container name is already in the URI path.
81+ // The ContainerName setting from config is ignored to avoid a mismatch.
82+ _logger . Warn ( "SAS URL container differs from configured ContainerName. Using URL container." ) ;
83+ return new BlobContainerClient ( uri ) ;
84+ }
85+
86+ // Account-level SAS URL — combine with the configured container name.
87+ return new BlobServiceClient ( uri ) . GetBlobContainerClient ( containerName ) ;
6188 }
89+
90+ // Classic connection string (AccountKey or Azurite emulator).
91+ // Container is auto-created and set to public blob access on first use.
92+ var client = new BlobContainerClient ( value , containerName ) ;
93+ client . CreateIfNotExists ( ) ;
94+ client . SetAccessPolicy ( PublicAccessType . BlobContainer ) ;
95+ return client ;
6296 }
6397
98+ private BlobContainerClient BlobContainerClient => _blobContainerClient . Value ;
99+
64100 private BlobClient GetBlobClient ( string blobName )
65101 {
66102 var directoryName = _configuration . GetSection ( CloudStorageName ) . GetValue < string > ( "DirectoryName" ) ;
67- return BlobContainerClient . GetBlobClient ( Path . Combine ( directoryName ?? "" , blobName ) ) ;
103+
104+ var normalizedDirectory = directoryName ? . Replace ( '\\ ' , '/' ) . Trim ( '/' ) ;
105+
106+ var normalizedBlobName = blobName . Replace ( '\\ ' , '/' ) . TrimStart ( '/' ) ;
107+
108+ var blobPath = string . IsNullOrWhiteSpace ( directoryName )
109+ ? normalizedBlobName
110+ : $ "{ normalizedDirectory } /{ normalizedBlobName } ";
111+ return BlobContainerClient . GetBlobClient ( blobPath ) ;
68112 }
69113
70- private string GetAzureFileName ( StoredFileVersion version ) => version . Id + version . FileType ;
114+ private static string GetAzureFileName ( StoredFileVersion version ) => version . Id + version . FileType ;
71115
72116 private async Task < Stream > GetStreamInternalAsync ( string filePath )
73117 {
@@ -109,7 +153,7 @@ public override Stream GetStream(StoredFileVersion fileVersion)
109153 public override async Task UpdateVersionContentAsync ( StoredFileVersion version , Stream stream )
110154 {
111155 if ( stream == null )
112- throw new Exception ( $ "{ nameof ( stream ) } must not be null") ;
156+ throw new ArgumentNullException ( $ "{ nameof ( stream ) } must not be null") ;
113157
114158 var blob = GetBlobClient ( GetAzureFileName ( version ) ) ;
115159
0 commit comments