我正在尝试为我的网站上的大文件创建一个下载系统。我正在使用 Next.js,我的大文件托管在 CDN 上。我需要的是从我的 CDN 下载几个文件,创建一个 zip 文件,然后将此存档文件发送给客户端。我已经使用以下结构进行了此操作:
/api/download.ts:
export const getS3Object = async (bucketParams: any) => {
try {
const response = await s3Client.send(new GetObjectCommand(bucketParams))
return response
} catch (err) {
console.log('Error', err)
}
}
async function downloadRoute(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
res.setHeader('Content-Disposition', 'attachment; filename=pixel_capture_HDR.zip')
res.setHeader('Content-Type', 'application/zip')
const fileKeys = req.body.files
const archive = archiver('zip', { zlib: { level: 9 } })
archive.on('error', (err: Error) => {
console.error('Error creating ZIP file:', err)
res.status(500).json({ error: 'Error creating ZIP file' })
})
archive.pipe(res)
try {
for (const fileKey of (fileKeys as string[])) {
const params = { Bucket: 'three-assets', Key: fileKey }
const s3Response = await getS3Object(params) as Buffer
archive.append(s3Response.Body, { name: fileKey.substring(fileKey.lastIndexOf('/') + 1) })
}
archive.finalize()
} catch (error) {
console.error('Error retrieving files from S3:', error)
res.status(500).json({ error: 'Error retrieving files from S3' })
}
} else {
res.status(405).json({ error: `Method '${req.method}' Not Allowed` })
}
}
export const config = { api: { responseLimit: false } }
export default withIronSessionApiRoute(downloadRoute, sessionOptions)
我的下载功能在pages/download.tsx中:
const handleDownload = async() => {
setLoading(true)
const files = [files_to_download]
const response = await fetch('/api/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ files: files })
})
if (response.ok) {
const blob = await response.blob()
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'pixel_capture_HDR.zip'
link.click()
URL.revokeObjectURL(url)
}
setLoading(false)
}
通过上述结构,当客户端开始获取文件时,我会在客户端上显示加载 UI。一旦一切准备就绪并由服务器压缩,zip 文件几乎会立即下载到用户的下载文件夹中。
这种结构的问题是,如果用户关闭浏览器选项卡,下载就会停止并失败。我希望实现的是将下载和压缩直接流式/管道传输到浏览器的下载队列中,如果这有意义的话。
我尝试在客户端使用 Readable Streams,这似乎很奇怪,因为当我记录推送函数时,我可以看到正在记录的值,并且当所有内容都下载完毕后,“done”变量从 false 变为 true。但是,当一切准备就绪时,仍然会触发 zip 文件的实际浏览器下载,而不是用户单击下载按钮后立即“流式传输”下载。
if (response.ok) {
const contentDispositionHeader = response.headers.get('Content-Disposition')
const stream = response.body
const reader = stream.getReader()
const readableStream = new ReadableStream({
start(controller) {
async function push() {
const { done, value } = await reader.read()
if (done) {
controller.close()
return
}
controller.enqueue(value)
push()
}
push()
}
})
const blob = new Blob([readableStream], { type: 'application/octet-stream' })
const url = URL.createObjectURL(blob)
}
我意识到解释我想要什么是相当具有挑战性的,我希望它是可以理解和实现的。
我将非常感谢有关如何实现此类解决方案的任何提示。
预先感谢您的帮助!
如果我理解正确的话,您的目标是启用即时生成的 ZIP 文件的流式下载,即使 ZIP 创建过程尚未完成,也允许用户开始下载。
我看到,在原始代码中,S3 中的每个文件都是使用
await getS3Object(params) as Buffer
完全提取到内存中的。
并且“所有”文件在添加到 ZIP 存档之前都存储在内存中。对于大文件或大量文件来说,这可能会成为问题。
ZIP 文件一次性创建并发送到客户端,直到所有文件都已获取并压缩后才会启动。
在服务器端,我建议将每个 S3 响应流直接通过管道传输到 ZIP 存档流。这样,一旦服务器开始从 S3 接收文件,它就会立即开始将其发送到客户端。您的
/api/download.ts
将是:
import { PassThrough } from 'stream';
async function downloadRoute(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
res.setHeader('Content-Disposition', 'attachment; filename=pixel_capture_HDR.zip');
res.setHeader('Content-Type', 'application/zip');
const fileKeys = req.body.files;
const archive = archiver('zip', { zlib: { level: 9 } });
archive.on('error', (err: Error) => {
console.error('Error creating ZIP file:', err);
res.status(500).json({ error: 'Error creating ZIP file' });
});
archive.pipe(res);
try {
for (const fileKey of (fileKeys as string[])) {
const params = { Bucket: 'three-assets', Key: fileKey };
const s3Response = await getS3Object(params);
const pass = new PassThrough();
pass.end(s3Response.Body);
archive.append(pass, { name: fileKey.substring(fileKey.lastIndexOf('/') + 1) });
}
archive.finalize();
} catch (error) {
console.error('Error retrieving files from S3:', error);
res.status(500).json({ error: 'Error retrieving files from S3' });
}
} else {
res.status(405).json({ error: `Method '${req.method}' Not Allowed` });
}
}
在客户端,客户端等待接收整个 ZIP 文件,然后再开始下载。
相反,您可以从
Blob
创建一个新的
ReadableStream
对象,并随着流的进行创建一个对象 URL。以下是如何在 pages/download.tsx
中修改客户端代码: if (response.ok) {
const contentDispositionHeader = response.headers.get('Content-Disposition');
const stream = response.body;
const reader = stream.getReader();
const chunks = [];
const pump = async () => {
const { done, value } = await reader.read();
if (done) {
const blob = new Blob(chunks, { type: 'application/zip' });
const url = URL.createObjectURL(blob);
// Your download logic
return;
}
chunks.push(value);
pump();
};
pump();
}
通过使用这些调整,ZIP 文件应在用户启动下载后立即开始流式传输。这样,即使用户关闭浏览器选项卡,下载也应该继续到用户关闭选项卡的位置。