首頁 > 軟體

JS實現可恢復的檔案上傳範例詳解

2022-12-29 14:01:06

正文

使用 fetch 方法來上傳檔案相當容易。

連線斷開後如何恢復上傳?這裡沒有對此的內建選項,但是我們有實現它的一些方式。

對於大檔案(如果我們可能需要恢復),可恢復的上傳應該帶有上傳進度提示。由於 fetch 不允許跟蹤上傳進度,我們將會使用 XMLHttpRequest

不太實用的進度事件

要恢復上傳,我們需要知道在連線斷開前已經上傳了多少。

我們有 xhr.upload.onprogress 來跟蹤上傳進度。

不幸的是,它不會幫助我們在此處恢復上傳,因為它會在資料 被傳送 時觸發,但是伺服器是否接收到了?瀏覽器並不知道。

或許它是由本地網路代理緩衝的(buffered),或者可能是遠端伺服器程序剛剛終止而無法處理它們,亦或是它在中間丟失了,並沒有到達伺服器。

這就是為什麼此事件僅適用於顯示一個好看的進度條。

要恢復上傳,我們需要 確切地 知道伺服器接收的位元組數。而且只有伺服器能告訴我們,因此,我們將發出一個額外的請求。

演演算法

首先,建立一個檔案 id,以唯一地標識我們要上傳的檔案:

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

在恢復上傳時需要用到它,以告訴伺服器我們要恢復的內容。

如果名稱,或大小,或最後一次修改時間發生了更改,則將有另一個 fileId

向伺服器傳送一個請求,詢問它已經有了多少位元組,像這樣:

let response = await fetch('status', {
  headers: {
    'X-File-Id': fileId
  }
});
// 伺服器已有的位元組數
let startByte = +await response.text();

這假設伺服器通過 X-File-Id header 跟蹤檔案上傳。應該在伺服器端實現。

如果伺服器上尚不存在該檔案,則伺服器響應應為 0

然後,我們可以使用 Blob 和 slice 方法來傳送從 startByte 開始的檔案:

xhr.open("POST", "upload", true);
// 檔案 id,以便伺服器知道我們要恢復的是哪個檔案
xhr.setRequestHeader('X-File-Id', fileId);
// 傳送我們要從哪個位元組開始恢復,因此伺服器知道我們正在恢復
xhr.setRequestHeader('X-Start-Byte', startByte);
xhr.upload.onprogress = (e) => {
  console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
};
// 檔案可以是來自 input.files[0],或者另一個源
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

let http = require('http');
let static = require('node-static');
let fileServer = new static.Server('.');
let path = require('path');
let fs = require('fs');
let debug = require('debug')('example:resume-upload');
let uploads = Object.create(null);
function onUpload(req, res) {
  let fileId = req.headers['x-file-id'];
  let startByte = +req.headers['x-start-byte'];
  if (!fileId) {
    res.writeHead(400, "No file id");
    res.end();
  }
  // 我們將「無處」儲存檔案
  let filePath = '/dev/null';
  // 可以改用真實路徑,例如
  // let filePath = path.join('/tmp', fileId);
  debug("onUpload fileId: ", fileId);
  // 初始化一個新上傳
  if (!uploads[fileId]) uploads[fileId] = {};
  let upload = uploads[fileId];
  debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte)
  let fileStream;
  // 如果 startByte 為 0 或者沒設定,建立一個新檔案,否則檢查大小並附加到現有的大小
  if (!startByte) {
    upload.bytesReceived = 0;
    fileStream = fs.createWriteStream(filePath, {
      flags: 'w'
    });
    debug("New file created: " + filePath);
  } else {
    // 我們也可以檢查磁碟上的檔案大小以確保
    if (upload.bytesReceived != startByte) {
      res.writeHead(400, "Wrong start byte");
      res.end(upload.bytesReceived);
      return;
    }
    // 附加到現有檔案
    fileStream = fs.createWriteStream(filePath, {
      flags: 'a'
    });
    debug("File reopened: " + filePath);
  }
  req.on('data', function(data) {
    debug("bytes received", upload.bytesReceived);
    upload.bytesReceived += data.length;
  });
  // 將 request body 傳送到檔案
  req.pipe(fileStream);
  // 當請求完成,並且其所有資料都以寫入完成
  fileStream.on('close', function() {
    if (upload.bytesReceived == req.headers['x-file-size']) {
      debug("Upload finished");
      delete uploads[fileId];
      // 可以在這裡對上傳的檔案進行其他操作
      res.end("Success " + upload.bytesReceived);
    } else {
      // 連線斷開,我們將未完成的檔案保留在周圍
      debug("File unfinished, stopped at " + upload.bytesReceived);
      res.end();
    }
  });
  // 如果發生 I/O error —— 完成請求
  fileStream.on('error', function(err) {
    debug("fileStream error");
    res.writeHead(500, "File error");
    res.end();
  });
}
function onStatus(req, res) {
  let fileId = req.headers['x-file-id'];
  let upload = uploads[fileId];
  debug("onStatus fileId:", fileId, " upload:", upload);
  if (!upload) {
    res.end("0")
  } else {
    res.end(String(upload.bytesReceived));
  }
}
function accept(req, res) {
  if (req.url == '/status') {
    onStatus(req, res);
  } else if (req.url == '/upload' && req.method == 'POST') {
    onUpload(req, res);
  } else {
    fileServer.serve(req, res);
  }
}
// -----------------------------------
if (!module.parent) {
  http.createServer(accept).listen(8080);
  console.log('Server listening at port 8080');
} else {
  exports.accept = accept;
}

uploader.js

class Uploader {
  constructor({file, onProgress}) {
    this.file = file;
    this.onProgress = onProgress;
    // 建立唯一標識檔案的 fileId
    // 我們還可以新增使用者對談識別符號(如果有的話),以使其更具唯一性
    this.fileId = file.name + '-' + file.size + '-' + file.lastModified;
  }
  async getUploadedBytes() {
    let response = await fetch('status', {
      headers: {
        'X-File-Id': this.fileId
      }
    });
    if (response.status != 200) {
      throw new Error("Can't get uploaded bytes: " + response.statusText);
    }
    let text = await response.text();
    return +text;
  }
  async upload() {
    this.startByte = await this.getUploadedBytes();
    let xhr = this.xhr = new XMLHttpRequest();
    xhr.open("POST", "upload", true);
    // 傳送檔案 id,以便伺服器知道要恢復哪個檔案
    xhr.setRequestHeader('X-File-Id', this.fileId);
    // 傳送我們要從哪個位元組開始恢復,因此伺服器知道我們正在恢復
    xhr.setRequestHeader('X-Start-Byte', this.startByte);
    xhr.upload.onprogress = (e) => {
      this.onProgress(this.startByte + e.loaded, this.startByte + e.total);
    };
    console.log("send the file, starting from", this.startByte);
    xhr.send(this.file.slice(this.startByte));
    // return
    //   true —— 如果上傳成功,
    //   false —— 如果被中止
    // 出現 error 時將其丟擲
    return await new Promise((resolve, reject) => {
      xhr.onload = xhr.onerror = () => {
        console.log("upload end status:" + xhr.status + " text:" + xhr.statusText);
        if (xhr.status == 200) {
          resolve(true);
        } else {
          reject(new Error("Upload failed: " + xhr.statusText));
        }
      };
      // onabort 僅在 xhr.abort() 被呼叫時觸發
      xhr.onabort = () => resolve(false);
    });
  }
  stop() {
    if (this.xhr) {
      this.xhr.abort();
    }
  }
}

index.html

<!DOCTYPE HTML>
<script src="uploader.js"></script>
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
  <input type="file" name="myfile">
  <input type="submit" name="submit" value="Upload (Resumes automatically)">
</form>
<button onclick="uploader.stop()">Stop upload</button>
<div id="log">Progress indication</div>
<script>
  function log(html) {
    document.getElementById('log').innerHTML = html;
    console.log(html);
  }
  function onProgress(loaded, total) {
    log("progress " + loaded + ' / ' + total);
  }
  let uploader;
  document.forms.upload.onsubmit = async function(e) {
    e.preventDefault();
    let file = this.elements.myfile.files[0];
    if (!file) return;
    uploader = new Uploader({file, onProgress});
    try {
      let uploaded = await uploader.upload();
      if (uploaded) {
        log('success');
      } else {
        log('stopped');
      }
    } catch(err) {
      console.error(err);
      log('error');
    }
  };
</script>

結果

正如我們所看到的,現代網路方法在功能上已經與檔案管理器非常接近 —— 控制 header,進度指示,傳送檔案片段等。

我們可以實現可恢復的上傳等。

以上就是JS實現可恢復的檔案上傳範例詳解的詳細內容,更多關於JS可恢復檔案上傳的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com