我的博客使用 Azure Blob Storage 存储文章配图,结果今天玩 Azure CDN 的时候爆了,原因是图片mime type不对。我们来看看如何在 .NET Core 里批量重置 Azure Blob Storage 中文件的mime type吧。
起因
使用编程方式(Azure Storage API)上传的文件,如果不指定 ContentType ,那么 Azure 不会聪明到根据文件拓展名设置 ContentType 。这个 ContentType 最终就是输出给浏览器的HTTP Header中的content-type,即Web服务器上的mime type。对于没有设置 ContentType 的文件,直接访问 Azure Blob 的地址,会返回"application/octet-stream"。不同浏览器对此处理方式不一样,大部分浏览器会调用文件下载,而不是打开文件。于是,图片就没法显示了。
我博客中的配图,以前之所以没问题,是因为没有使用CDN让客户端直接读取图片,而是通过后台处理,会自动加上正确的mime type,因此这个问题一直没暴露。
获取文件的 ContentType
.NET Core 没有 MimeMapping.GetMimeMapping() 这个API,因此我们需要一个workaround。
感谢长沙.NET技术社区成员 @刘命汉 的发现以及 @周杰 的验证,ASP.NET Core 自带的 FileExtensionContentTypeProvider 是个可替代方案。
var pvd = new FileExtensionContentTypeProvider();
bool isKnownType = pvd.TryGetContentType("test.png", out string mimeType);
// mimeType: "image/png"
对于不认识的拓展名,TryGetContentType() 会返回 false | null
而 CloudBlockBlob 不设置 ContentType 的话会返回默认的 application/octet-stream,因此null不影响。
更改文件的 ContentType
对于已经上传到 Azure Blob Storage 的文件,可以通过编程方式更改 ContentType 。获取到 CloudBlockBlob 对象以后,设置 Properties.ContentType,然后调用 SetPropertiesAsync() 方法保存更改即可。
对于未上传到Azure的文件,设置完 ContentType 以后,不需要调用 SetPropertiesAsync(), 上传操作 UploadFromStreamAsync() 会带上这些属性。
参见我博客代码commit: https://github.com/EdiWang/Moonglade/commit/3508e35055ae33b2c2241d93f615283a109bad85
自制开源工具
我今天抽空写了个批量重置 Azure Blob Storage 文件Mime Type 的工具,已在 GitHub 开源:
https://github.com/EdiWang/Azure-Blob-MimeType-Reset
关键代码
获取 CloudBlobContainer
有了 CloudBlobContainer 才能遍历里面的文件
private static CloudBlobContainer GetBlobContainer()
{
CloudStorageAccount storageAccount = new CloudStorageAccount(new StorageCredentials(Options.AccountName, Options.AccountKey), true);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference(Options.ContainerName);
return container;
}
修改ContentType
此处我做了个判断,只有 ContentType 不正确的文件,才重置 ContentType
private static CloudBlockBlob TrySetContentType(CloudBlockBlob blob, string contentType)
{
if (blob.Properties.ContentType.ToLower() != contentType)
{
blob.Properties.ContentType = contentType;
return blob;
}
return null;
}
遍历文件及提交更改
var pvd = new FileExtensionContentTypeProvider();
WriteMessage($"[{DateTime.Now}] Updating Mime Type...");
int affectedFilesCount = 0;
foreach (var blob in BlobContainer.ListBlobs().OfType<CloudBlockBlob>())
{
string extension = Path.GetExtension(blob.Uri.AbsoluteUri).ToLower();
bool isKnownType = pvd.TryGetContentType(extension, out string mimeType);
if (isKnownType)
{
if (TrySetContentType(blob, mimeType) != null)
{
WriteMessage($"[{DateTime.Now}] Updating {blob.Uri.AbsoluteUri} => {mimeType}");
await blob.SetPropertiesAsync();
affectedFilesCount ;
}
}
}