问题描述
对于在我正在开发的应用程序上启动文件下载的最佳策略是什么,我有点困惑。后端(django RESTful API)和前端(angular 9)都可以进行修改,以最好的方式实现这一点。
最低背景: 应用程序需要允许用户下载文件。目前,后端允许对其存储文件的方式进行多种配置——文件可以存储在 API 主机上,也可以存储在存储桶中(此处为谷歌云,但这应该无关紧要)。
显然,客户不应该关心如何存储这些文件。他们应该能够转到像 https://example.com/api/files/<file UUID>/download/
这样的端点开始下载。 正如目前所写,当后端收到请求时,它会根据使用的存储类型做出不同的响应。如果文件存储在服务器上,它只用文件内容响应:
def get(self,request,*args,**kwargs):
...
contents = open(filepath,'rb')
mime_type,_ = mimetypes.guess_type(filepath)
response = HttpResponse(content = contents)
response['Content-Type'] = mime_type
response['Content-disposition'] = 'attachment; filename=%s' % os.path.basename(filepath)
return response
另一方面,如果文件存储在存储桶中,我会使用谷歌云 SDK 创建一个签名 URL,并返回一个 302,将该签名 URL 作为重定向位置。
如果我直接与 API 交互,那一切都很好。一旦我涉及 Angular,我不知道如何将其连接起来。理想情况下,UI 应该只是一个按钮——用户点击并开始下载。
基于查看不同的来源(特别是 Angular 6 Downloading file from rest api),我想我可以通过创建自定义 angular 指令来尝试这个。例如,HTML 有:
<a fileDownload [resourceId]="row.id">
<button [disabled]="!row.is_active">
Download
</button>
</a>
而 fileDownload
指令(部分)类似于:
@Directive({
selector: '[fileDownload]',exportAs: 'fileDownload',})
export class FileDownloadDirective implements OnDestroy {
@input() resourceId: string;
private destroy$: Subject<void> = new Subject<void>();
constructor(private ref: ElementRef,private fileService: FileService) {}
@HostListener('click')
onClick(): void {
this.fileService.downloadFile(this.resourceId)
.subscribe(x => {
// what should go here????
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
而我的 fileService
方法是:
downloadFile(id: number | string): Observable<any> {
return this.httpClient.get(`${this.API_URL}/resources/download/${id}/`);
}
正如我在上面的代码中所评论的那样,我不确定订阅时与响应有什么关系(如果有的话)。我在引用的 SO 线程中尝试了该版本,但无论我在订阅中做什么,我都会收到 CORS 错误。我在那里放了一些 console.log
语句,但它们没有出现,所以我什至没有进入 subscribe
回调。
无论如何,浏览器都会拦截302,然后立即尝试跟随重定向。这会导致 CORS 失败,我似乎无法修复。基于此 https://github.com/googleapis/python-storage/issues/3 ,我查看了预检选项请求。由于没有 CORS Access-Control-Allow-Origin
标头,浏览器的检查器显示此失败。请求是:
OPTIONS /my-bucket/...<snip>...
Host: storage.googleapis.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip,deflate,br
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization
Referer: http://example.com:4200/
Origin: http://example.com:4200
Connection: keep-alive
和响应
HTTP/2 200 OK
x-guploader-uploadid: <...>
date: Sat,20 Mar 2021 12:22:03 GMT
expires: Sat,20 Mar 2021 12:22:03 GMT
cache-control: private,max-age=0
content-length: 0
server: UploadServer
content-type: text/html; charset=UTF-8
alt-svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
X-Firefox-Spdy: h2
值得注意的是,没有 Access-Control-Allow-Origin
标头,浏览器不会继续处理对文件的实际 GET
请求。
使用 cURL,我可以复制 OPTIONS
请求不返回 Access-Control-Allow-Origin
标头。签名的 URL 有效,因为我可以通过 GET
请求使用 cURL 下载文件。
据我所知,我相信我的存储桶设置正确:
$ gsutil cors get gs://my-bucket
[{"maxAgeSeconds": 3600,"method": ["GET"],"origin": ["*"],"responseHeader": ["Content-Type","Access-Control-Allow-Origin"]}]
有没有更好的方法来做到这一点?我想我可以编写一个 HttpInterceptor
来捕获 302 并修改请求。或者最好不要让 API 以 302 响应?由于我告诉它从 google 存储桶存储中获取文件,这似乎是最理想的响应,因为我有效地重定向了请求。
感谢您的任何帮助/建议!
解决方法
可能是 fileService
方法中的请求不正确。检查 documentation,向 Cloud Storage API 发出下载文件的请求:
https://storage.googleapis.com/download/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME?alt=media
用适当的值替换 BUCKET_NAME
和 OBJECT_NAME
。
此外,您可能会发现这个 NodeJS sample code 很有用,它演示了如何从 Cloud Storage 下载文件:
// The ID of your GCS bucket
// const bucketName = 'your-unique-bucket-name';
// The ID of your GCS file
// const fileName = 'your-file-name';
// The path to which the file should be downloaded
// const destFileName = '/local/path/to/file.txt';
// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
// Creates a client
const storage = new Storage();
async function downloadFile() {
const options = {
destination: destFileName,};
// Downloads the file
await storage.bucket(bucketName).file(fileName).download(options);
console.log(
`gs://${bucketName}/${fileName} downloaded to ${destFileName}.`
);
}
downloadFile().catch(console.error);