如何从Azure Blob存储下载文件到浏览器

42

我已经成功地列出了可用文件,但我需要知道如何将该文件传递给浏览器,以便用户下载,而不必将其保存到服务器上。

这是我获取文件列表的方法

var azureConnectionString = CloudConfigurationManager.GetSetting("AzureBackupStorageConnectString");
var containerName = ConfigurationManager.AppSettings["FmAzureBackupStorageContainer"];
if (azureConnectionString == null || containerName == null)
    return null;

CloudStorageAccount backupStorageAccount = CloudStorageAccount.Parse(azureConnectionString);
var backupBlobClient = backupStorageAccount.CreateCloudBlobClient();
var container = backupBlobClient.GetContainerReference(containerName); 
var blobs = container.ListBlobs(useFlatBlobListing: true);
var downloads = blobs.Select(blob => blob.Uri.Segments.Last()).ToList();
4个回答

77

虽然Blob内容可以通过Web服务器流式传输,并通过浏览器传递给最终用户,但这种解决方案会对Web服务器产生负担,包括CPU和NIC。

另一种方法是为最终用户提供所需Blob的URI以便下载,在HTML内容中他们可以点击该链接。例如:https://myaccount.blob.core.windows.net/mycontainer/myblob.ext

问题在于,如果内容是私有的,上述URI将无法使用,除非使用公共Blob。为此,您可以创建Shared Access Signature(或服务器存储的策略),然后将哈希查询字符串附加到URI上。这个新的URI将在一定时间内有效(例如10分钟)。

这里是一个为Blob创建SAS的小例子:

var sasConstraints = new SharedAccessBlobPolicy();
sasConstraints.SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-5);
sasConstraints.SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(10);
sasConstraints.Permissions = SharedAccessBlobPermissions.Read;

var sasBlobToken = blob.GetSharedAccessSignature(sasConstraints);

return blob.Uri + sasBlobToken;

请注意,开始时间被设置为几分钟之前,这是为了处理时钟漂移。这是我从完整教程中获取/修改此代码示例。

通过使用直接blob访问,您将完全绕开VM / web角色实例/网站实例(从而减少服务器负载),并使您的最终用户直接从blob存储中提取blob内容。您仍然可以使用Web应用程序来处理权限,决定要传递的内容等。但是...这使您可以直接链接到blob资源,而不是通过Web服务器将它们流式传输。


2
更多关于钥匙托管模式的信息请参见:https://msdn.microsoft.com/zh-cn/library/dn568102.aspx - Sascha Gottfried
当我使用URI下载文件时遇到错误,我必须采取类似的方法。感谢您提供的提示,David。 - Kris
我知道这篇文章已经有几年了,但是我在下载多个文件并将它们压缩时遇到了问题。 我提供单个文件下载的SAS Urls - 这很好用,但是当用户想要下载100个图像文件时,我不想向浏览器提供100个SAS URls进行下载,我想将它们合并成一个zip文件。我希望能够在不完全冻结我的服务器的情况下完成这个任务,但是我还没有找到一个前端框架支持使用客户端资源压缩blob文件。 非常感谢您的指导。 - George Harnwell
如果我们需要在浏览器中预览文件而不将实际文件下载到用户本地存储,我们该如何修改上面的代码片段? - Chaitanya Sairam
我将我的 blob 容器访问级别设置为“容器和 Blob 的公共读取访问权限”。但是,当在我的网站上向最终用户提供 HTML5 下载链接以访问该 Blob 时,却收到了 401 未授权错误。顺便说一下,我正在使用 Azure.Storarge.Blobs v12 nuget 包。也许 SAS 方法可以解决问题,但即便将该 Blob 和容器的公共访问级别设为 public,提供 URL 仍然无法正常工作。 - kimbaudi
@kimbaudi - 请发布一个新问题,包含所有相关细节,而不是在另一个答案的评论中发布新问题(该问题与我回答的问题并没有直接关系)。此外,我的答案是7年前给出的,使用完全不同版本的SDK。 - David Makogon

13

一旦用户点击文件,服务器将响应:

var blob = container.GetBlobReferenceFromServer(option);

var memStream = new MemoryStream();
blob.DownloadToStream(memStream);

Response.ContentType = blob.Properties.ContentType;
Response.AddHeader("Content-Disposition", "Attachment;filename=" + option);
Response.AddHeader("Content-Length", blob.Properties.Length.ToString());
Response.BinaryWrite(memStream.ToArray());

非常感谢 Dhananjay Kumar 提供的解决方案。


6
那么,你要意识到这样做会导致整个 blob 的内容通过你的服务器路由,对吗?也就是说,blob 的内容将从存储中心传输到你的 VM/网站/Web 角色实例,然后通过 IIS/OWIN等传输到最终用户。 - David Makogon
2
你会推荐什么?我不能让我的最终用户访问整个存储,所以 Azure 存储资源管理器不适用。 - stackoverfloweth
4
我发表了一个备选答案。 - David Makogon
2
我发现这个方法很慢,因为第一个字节要等到最后一个字节从 Blob 存储中获取后才能传输到浏览器。使用 Andy 的两行代码可以减少内存开销并降低延迟 10 毫秒! - Daniel Bailey

12

如果您使用 ASP.NET(核心),则可以使用 FileStreamResult 流式传输内容到浏览器而无需在服务器上保存文件,这是更优雅的解决方案并且它是一个 IActionResult。

var stream = await blob.OpenReadAsync();
return File(stream, blob.Properties.ContentType, option);

2
如果您不想冒险让SAS令牌被盗,那么这是最好的解决方案,毕竟它们只是在查询字符串中,即使使用https也没有加密。此外,如果您正在流式传输安全视频,请在返回文件对象之前将EnableRangeProcessing设置为开启状态,这将允许浏览器跳过整个文件而不必下载它! - Daniel Bailey
2
@DanielBailey 我不同意,这会使整个 Blob 被不必要地下载到您的应用程序中,从而消耗您服务器的带宽,也会使过程变慢。您可以像 David 的答案一样在 SAS 令牌上设置到期日期,而且您不能只是窃取一个令牌并阅读任何内容,附加到 Uri 的令牌仅适用于该 Blob,这意味着任何具有该 Uri 的人都可以使用来自您的应用程序的 Uri 执行上面的 Andy 代码。 - Alisson Reinaldo Silva
3
有点神奇,但整个 Blob 并没有被下载,而是只下载了指定的区域。就好像 Blob 存储知道你只需要文件的一部分,只会将该部分流式传输到主机,然后主机仅使用一个小缓冲区将其转发出去。 - Daniel Bailey
3
为了防止基本日志记录工具立即下载访问点通过的任何内容,您需要在SAS令牌上设置相当短的到期日期。但是也不能太短,因为浏览器需要时间进行往返并将请求直接发送回Blob存储。如果您在https下进行POST,他们将需要解密消息才能获取必要的密钥以及访问Blob,我认为这样更安全,但我不是SAS令牌保护的专家。一定有一种安全的方法来实现它,但使用GET则不是其中之一。 - Daniel Bailey
1
@Alisson,你忘记了需要授权下载的情况。SAS令牌无法提供此功能。登录您的应用程序的某个人可以使用SAS令牌泄露这些链接,从而使每个人都能够下载文件。当它通过您的端点时,未经身份验证/未经授权的用户将收到401/403错误。 - Piotr Perak
我正在使用.NET 6(Core),这个解决方案可以工作,但是提供具有公共访问级别的 blob 链接会导致401未经授权的错误(即使 blob 和容器具有公共访问级别)。顺便说一下,我正在使用Azure.Storage.Blobs v12 nuget包。另外,我还没有尝试过SAS方法。 - kimbaudi

1

我已经做了一个示例,您可以上传和下载Blob文件。

using System;
using System.Threading.Tasks;
using System.IO;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;

namespace GetBackup
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string Config_string = "";

            using (StreamReader SourceReader = File.OpenText(@"appsettings.json"))
            {
                Config_string = await SourceReader.ReadToEndAsync();
            }

            var config = (JObject)JsonConvert.DeserializeObject(Config_string);

            if(config["Application_type"].ToString()== "Backup")
            {
                string Dir_path = config["Backup_Path"].ToString();
                string[] allfiles = Directory.GetFiles(Dir_path, "*.*", SearchOption.AllDirectories);


                string storageConnectionString = config["AZURE_STORAGE_CONNECTION_STRING"].ToString();
                CloudStorageAccount storageAccount;
                if (CloudStorageAccount.TryParse(storageConnectionString, out storageAccount))
                {
                    CloudBlobClient cloudBlobClient = storageAccount.CreateCloudBlobClient();
                    CloudBlobContainer cloudBlobContainer = cloudBlobClient.GetContainerReference("rtddata");
                    //await cloudBlobContainer.CreateAsync();

                    string[] ExcludeFiles = config["Exception_File"].ToString().Split(',');

                    foreach (var file in allfiles)
                    {
                        FileInfo info = new FileInfo(file);
                        if (!ExcludeFiles.Contains(info.Name))
                        {
                            string folder = (Dir_path.Length < info.DirectoryName.Length) ? info.DirectoryName.Replace(Dir_path, "") : "";
                            folder = (folder.Length > 0) ? folder + "/" : "";
                            CloudBlockBlob cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(folder + info.Name);
                            await cloudBlockBlob.UploadFromFileAsync(info.FullName);
                        }

                    }

                }
            }
            
            else if (config["Application_type"].ToString() == "Restore")
            {
                string storageConnectionString = config["AZURE_STORAGE_CONNECTION_STRING"].ToString();
                CloudStorageAccount storageAccount;
               
                if (CloudStorageAccount.TryParse(storageConnectionString, out storageAccount))
                {
                    CloudBlobClient cloudBlobClient = storageAccount.CreateCloudBlobClient();
                    CloudBlobContainer cloudBlobContainer = cloudBlobClient.GetContainerReference("rtddata");
                    string Dir_path = config["Restore_Path"].ToString();

                    IEnumerable<IListBlobItem> results = cloudBlobContainer.ListBlobs(null,true);  
                    foreach (IListBlobItem item in results)
                    {
                        string name = ((CloudBlockBlob)item).Name;
                        if (name.Contains('/'))
                        {
                            string[] subfolder = name.Split('/');
                            if (!Directory.Exists(Dir_path + subfolder[0]))
                            {
                                Directory.CreateDirectory(Dir_path + subfolder[0]);
                            }
                            
                        }  
                            CloudBlockBlob blockBlob = cloudBlobContainer.GetBlockBlobReference(name);
                            string path = (Dir_path + name);
                            blockBlob.DownloadToFile(path, FileMode.Create);
                    }
                    

                }
                    
            }

            
            
        }
    }
}

您好,我使用的“恢复”代码几乎与下载文件的相同,并且它完美地工作。问题在于,我下载的文件被用作 tmp 文件,因此我需要删除它,但使用这段代码会出现 dispose 错误。也许您能帮我解决这个问题吗? - Kevtho

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接