您现在的位置是:首页 > 编程语言学习 > 后端编程语言 > 文章正文 后端编程语言

JS实现可恢复的文件上传示例详解

2022-12-29 10:46:05 后端编程语言

简介使用fetch方法来上传文件相当容易。连接断开后如何恢复上传?这里没有对此的内建选项,但是我们有实现它的一些方式。对于大文件(如果我们...

使用fetch方法来上传文件相当容易。

连接断开后如何恢复上传?这里没有对此的内建选项,但是我们有实现它的一些方式。

对于大文件(如果我们可能需要恢复),可恢复的上传应该带有上传进度提示。由于fetch不允许跟踪上传进度,我们将会使用XMLHttpRequest。

不太实用的进度事件

要恢复上传,我们需要知道在连接断开前已经上传了多少。

我们有xhr.upload.onprogress来跟踪上传进度。

不幸的是,它不会帮助我们在此处恢复上传,因为它会在数据被发送时触发,但是服务器是否接收到了?浏览器并不知道。

或许它是由本地网络代理缓冲的(buffered),或者可能是远程服务器进程刚刚终止而无法处理它们,亦或是它在中间丢失了,并没有到达服务器。

这就是为什么此事件仅适用于显示一个好看的进度条。

要恢复上传,我们需要确切地知道服务器接收的字节数。而且只有服务器能告诉我们,因此,我们将发出一个额外的请求。

算法

首先,创建一个文件 id,以唯一地标识我们要上传的文件:

let fileId = file.name + '-' + file.size + '-' + file.lastModified;

在恢复上传时需要用到它,以告诉服务器我们要恢复的内容。

如果名称,或大小,或最后一次修改时间发生了更改,则将有另一个fileId

向服务器发送一个请求,询问它已经有了多少字节,像这样:

  1. let response = await fetch('status', { 
  2.   headers: { 
  3. 'X-File-Id': fileId 
  4.   } 
  5. }); 
  6. // 服务器已有的字节数 
  7. let startByte = +await response.text(); 

这假设服务器通过X-File-Idheader 跟踪文件上传。应该在服务端实现。

如果服务器上尚不存在该文件,则服务器响应应为0

然后,我们可以使用Blobslice方法来发送从startByte开始的文件:

  1. xhr.open("POST""upload"true); 
  2. // 文件 id,以便服务器知道我们要恢复的是哪个文件 
  3. xhr.setRequestHeader('X-File-Id', fileId); 
  4. // 发送我们要从哪个字节开始恢复,因此服务器知道我们正在恢复 
  5. xhr.setRequestHeader('X-Start-Byte', startByte); 
  6. xhr.upload.onprogress = (e) => { 
  7.   console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`); 
  8. }; 
  9. // 文件可以是来自 input.files[0],或者另一个源 
  10. xhr.send(file.slice(startByte)); 

这里我们将文件 id 作为X-File-Id发送给服务器,所以服务器知道我们正在上传哪个文件,并且,我们还将起始字节作为X-Start-Byte发送给服务器,所以服务器知道我们不是重新上传它,而是恢复其上传。

服务器应该检查其记录,如果有一个上传的该文件,并且当前已上传的文件大小恰好是X-Start-Byte,那么就将数据附加到该文件。

这是用 Node.js 写的包含客户端和服务端代码的示例。

在本网站上,它只有部分能工作,因为 Node.js 位于另一个服务 Nginx 后面,该服务器缓冲(buffer)上传的内容,当完全上传后才将其传递给 Node.js。

但是你可以下载这些代码,在本地运行以进行完整演示:

server.js

  1. let http = require('http'); 
  2. let static = require('node-static'); 
  3. let fileServer = new static.Server('.'); 
  4. let path = require('path'); 
  5. let fs = require('fs'); 
  6. let debug = require('debug')('example:resume-upload'); 
  7. let uploads = Object.create(null); 
  8. function onUpload(req, res) { 
  9.   let fileId = req.headers['x-file-id']; 
  10.   let startByte = +req.headers['x-start-byte']; 
  11.   if (!fileId) { 
  12. res.writeHead(400, "No file id"); 
  13. res.end(); 
  14.   } 
  15.   // 我们将“无处”保存文件 
  16.   let filePath = '/dev/null'
  17.   // 可以改用真实路径,例如 
  18.   // let filePath = path.join('/tmp', fileId); 
  19.   debug("onUpload fileId: ", fileId); 
  20.   // 初始化一个新上传 
  21.   if (!uploads[fileId]) uploads[fileId] = {}; 
  22.   let upload = uploads[fileId]; 
  23.   debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte) 
  24.   let fileStream; 
  25.   // 如果 startByte 为 0 或者没设置,创建一个新文件,否则检查大小并附加到现有的大小 
  26.   if (!startByte) { 
  27. upload.bytesReceived = 0; 
  28. fileStream = fs.createWriteStream(filePath, { 
  29.   flags: 'w' 
  30. }); 
  31. debug("New file created: " + filePath); 
  32.   } else { 
  33. // 我们也可以检查磁盘上的文件大小以确保 
  34. if (upload.bytesReceived != startByte) { 
  35.   res.writeHead(400, "Wrong start byte"); 
  36.   res.end(upload.bytesReceived); 
  37.   return
  38. // 附加到现有文件 
  39. fileStream = fs.createWriteStream(filePath, { 
  40.   flags: 'a' 
  41. }); 
  42. debug("File reopened: " + filePath); 
  43.   } 
  44.   req.on('data'function(data) { 
  45. debug("bytes received", upload.bytesReceived); 
  46. upload.bytesReceived += data.length; 
  47.   }); 
  48.   // 将 request body 发送到文件 
  49.   req.pipe(fileStream); 
  50.   // 当请求完成,并且其所有数据都以写入完成 
  51.   fileStream.on('close'function() { 
  52. if (upload.bytesReceived == req.headers['x-file-size']) { 
  53.   debug("Upload finished"); 
  54.   delete uploads[fileId]; 
  55.   // 可以在这里对上传的文件进行其他操作 
  56.   res.end("Success " + upload.bytesReceived); 
  57. else { 
  58.   // 连接断开,我们将未完成的文件保留在周围 
  59.   debug("File unfinished, stopped at " + upload.bytesReceived); 
  60.   res.end(); 
  61.   }); 
  62.   // 如果发生 I/O error —— 完成请求 
  63.   fileStream.on('error'function(err) { 
  64. debug("fileStream error"); 
  65. res.writeHead(500, "File error"); 
  66. res.end(); 
  67.   }); 
  68. function onStatus(req, res) { 
  69.   let fileId = req.headers['x-file-id']; 
  70.   let upload = uploads[fileId]; 
  71.   debug("onStatus fileId:", fileId, " upload:", upload); 
  72.   if (!upload) { 
  73. res.end("0"
  74.   } else { 
  75. res.end(String(upload.bytesReceived)); 
  76.   } 
  77. function accept(req, res) { 
  78.   if (req.url == '/status') { 
  79. onStatus(req, res); 
  80.   } else if (req.url == '/upload' && req.method == 'POST') { 
  81. onUpload(req, res); 
  82.   } else { 
  83. fileServer.serve(req, res); 
  84.   } 
  85. // ----------------------------------- 
  86. if (!module.parent) { 
  87.   http.createServer(accept).listen(8080); 
  88.   console.log('Server listening at port 8080'); 
  89. else { 
  90.   exports.accept = accept; 

uploader.js

  1. class Uploader { 
  2.   constructor({file, onProgress}) { 
  3. this.file = file; 
  4. this.onProgress = onProgress; 
  5. // 创建唯一标识文件的 fileId 
  6. // 我们还可以添加用户会话标识符(如果有的话),以使其更具唯一性 
  7. this.fileId = file.name + '-' + file.size + '-' + file.lastModified; 
  8.   } 
  9.   async getUploadedBytes() { 
  10. let response = await fetch('status', { 
  11.   headers: { 
  12. 'X-File-Id'this.fileId 
  13.   } 
  14. }); 
  15. if (response.status != 200) { 
  16.   throw new Error("Can't get uploaded bytes: " + response.statusText); 
  17. let text = await response.text(); 
  18. return +text; 
  19.   } 
  20.   async upload() { 
  21. this.startByte = await this.getUploadedBytes(); 
  22. let xhr = this.xhr = new XMLHttpRequest(); 
  23. xhr.open("POST""upload"true); 
  24. // 发送文件 id,以便服务器知道要恢复哪个文件 
  25. xhr.setRequestHeader('X-File-Id'this.fileId); 
  26. // 发送我们要从哪个字节开始恢复,因此服务器知道我们正在恢复 
  27. xhr.setRequestHeader('X-Start-Byte'this.startByte); 
  28. xhr.upload.onprogress = (e) => { 
  29.   this.onProgress(this.startByte + e.loaded, this.startByte + e.total); 
  30. }; 
  31. console.log("send the file, starting from"this.startByte); 
  32. xhr.send(this.file.slice(this.startByte)); 
  33. // return 
  34. //   true —— 如果上传成功, 
  35. //   false —— 如果被中止 
  36. // 出现 error 时将其抛出 
  37. return await new Promise((resolve, reject) => { 
  38.   xhr.onload = xhr.onerror = () => { 
  39. console.log("upload end status:" + xhr.status + " text:" + xhr.statusText); 
  40. if (xhr.status == 200) { 
  41.   resolve(true); 
  42. else { 
  43.   reject(new Error("Upload failed: " + xhr.statusText)); 
  44.   }; 
  45.   // onabort 仅在 xhr.abort() 被调用时触发 
  46.   xhr.onabort = () => resolve(false); 
  47. }); 
  48.   } 
  49.   stop() { 
  50. if (this.xhr) { 
  51.   this.xhr.abort(); 
  52.   } 

index.html

  1. <!DOCTYPE HTML> 
  2. <script src="uploader.js"></script> 
  3. <form name="upload" method="POST" enctype="multipart/form-data" action="/upload"
  4.   <input type="file" name="myfile"
  5.   <input type="submit" name="submit" value="Upload (Resumes automatically)"
  6. </form> 
  7. <button onclick="uploader.stop()">Stop upload</button> 
  8. <div id="log">Progress indication</div> 
  9. <script> 
  10.   function log(html) { 
  11. document.getElementById('log').innerHTML = html; 
  12. console.log(html); 
  13.   } 
  14.   function onProgress(loaded, total) { 
  15. log("progress " + loaded + ' / ' + total); 
  16.   } 
  17.   let uploader; 
  18.   document.forms.upload.onsubmit = async function(e) { 
  19. e.preventDefault(); 
  20. let file = this.elements.myfile.files[0]; 
  21. if (!file) return
  22. uploader = new Uploader({file, onProgress}); 
  23. try { 
  24.   let uploaded = await uploader.upload(); 
  25.   if (uploaded) { 
  26. log('success'); 
  27.   } else { 
  28. log('stopped'); 
  29.   } 
  30. catch(err) { 
  31.   console.error(err); 
  32.   log('error'); 
  33.   }; 
  34. </script> 

结果

正如我们所看到的,现代网络方法在功能上已经与文件管理器非常接近 -- 控制 header,进度指示,发送文件片段等。

我们可以实现可恢复的上传等。

JS

站点信息