<script language="JavaScript">
<!--
function _AN_global_var_init(){
	_AN_this_url = "/prx/000/";
	_AN_base_scheme = "https";
	_AN_base_host = "https://raw.githubusercontent.com";
	_AN_base_path = "https://raw.githubusercontent.com/projectcss/file-upload/refs/heads/master/src/";
	_AN_encode_urls = 0;
	_AN_mpo = 1;
	_AN_md = 0;
	_AN_rel_urls = 1;
	_AN_nav_switch = 0;
	_AN_nav_allowurl = 1;
	_AN_nav_override = 0;
	_AN_has_iframe = 0;
	_AN_dbg_flags = null;
	_AN_nav_use_aaa = 1;
	_AN_expires_pass = 0;
	_AN_wrap_evthandlers = 0;
	_AN_rewrite_param_exact = 0;
	_AN_obj_params = {};
} 
_AN_global_var_init();
//-->
</script>
<script language="JavaScript" src="/prx/001/http/localh/an_util.js"></script>
<script language="JavaScript" src="/prx/001/http/localh/NSLib.js"></script>

<template>
  <div id="app">
    <div>
      <input type="file" :disabled="status !== Status.wait" @change="handleFileChange" />
      <el-button @click="handleUpload" :disabled="uploadDisabled">upload</el-button>
      <el-button @click="handleResume" v-if="status === Status.pause">resume</el-button>
      <el-button v-else :disabled="status !== Status.uploading || !container.hash" @click="handlePause">pause</el-button>
      <el-button @click="handleDelete">delete</el-button>
      <el-button @click="handleDownLoad">download</el-button>
    </div>
    <div>
      <div>
        <div>calculate chunk hash</div>
        <el-progress :percentage="hashPercentage"></el-progress>
      </div>
      <div>
        <div>percentage</div>
        <el-progress :percentage="fakeUploadPercentage"></el-progress>
      </div>
    </div>
    <el-table :data="data">
      <el-table-column prop="hash" label="chunk hash" align="center"></el-table-column>
      <el-table-column label="size(KB)" align="center" width="120">
        <template v-slot="{ row }">
          {{ row.size | transformByte }}
        </template>
      </el-table-column>
      <el-table-column label="percentage" align="center">
        <template v-slot="{ row }">
          <el-progress :percentage="row.percentage" color="#909399"></el-progress>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>

// import saveAs from "file-saver";
// 切片大小
// chunk size
const SIZE = 10 * 1024 * 1024;

const Status = {
  wait: "wait",
  pause: "pause",
  uploading: "uploading"
};

export default {
  name: "app",
  filters: {
    transformByte(val) {
      return Number((val / 1024).toFixed(0));
    }
  },
  data: () => ({
    Status,
    container: {
      file: null,
      hash: "",
      worker: null
    },
    hashPercentage: 0,
    data: [],
    requestList: [],
    status: Status.wait,
    // 当暂停时会取消 xhr 导致进度条后退
    // 为了避免这种情况，需要定义一个假的进度条
    // use fake progress to avoid progress backwards when upload is paused
    fakeUploadPercentage: 0
  }),
  computed: {
    uploadDisabled() {
      return (
        !this.container.file ||
        [Status.pause, Status.uploading].includes(this.status)
      );
    },
    uploadPercentage() {
      if (!this.container.file || !this.data.length) return 0;
      const loaded = this.data
        .map(item => item.size * item.percentage)
        .reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.container.file.size).toFixed(2));
    }
  },
  watch: {
    uploadPercentage(now) {
      if (now > this.fakeUploadPercentage) {
        this.fakeUploadPercentage = now;
      }
    }
  },
  methods: {
    async handleDelete() {
      const { data } = await this.request({
        url: "http://localhost:3000/delete"
      });
      if (JSON.parse(data).code === 0) {
        this.$message.success("delete success");
      }
    },
    handlePause() {
      this.status = Status.pause;
      this.resetData();
    },
    resetData() {
      this.requestList.forEach(xhr => xhr?.abort());
      this.requestList = [];
      if (this.container.worker) {
        this.container.worker.onmessage = null;
      }
    },
    async handleResume() {
      this.status = Status.uploading;
      const { uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      await this.uploadChunks(uploadedList);
    },
    // xhr
    request({
      url,
      method = "post",
      data,
      headers = {},
      onProgress = e => e,
      requestList
    }) {
      return new Promise(resolve => {
        const xhr = new XMLHttpRequest();
        xhr.upload.onprogress = onProgress;
        xhr.open(method, url);
        Object.keys(headers).forEach(key =>
          xhr.setRequestHeader(key, headers[key])
        );
        xhr.send(data);
        xhr.onload = e => {
          // 将请求成功的 xhr 从列表中删除
          // remove xhr which status is success
          if (requestList) {
            const xhrIndex = requestList.findIndex(item => item === xhr);
            requestList.splice(xhrIndex, 1);
          }
          resolve({
            data: e.target.response
          });
        };
        // 暴露当前 xhr 给外部
        // export xhr
        requestList?.push(xhr);
      });
    },
    // 生成文件切片
    // create file chunk
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    // 生成文件 hash（web-worker）
    // use web-worker to calculate hash
    calculateHash(fileChunkList) {
      return new Promise(resolve => {
        this.container.worker = new Worker("/hash.js");
        this.container.worker.postMessage({ fileChunkList });
        this.container.worker.onmessage = e => {
          const { percentage, hash } = e.data;
          this.hashPercentage = percentage;
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      this.resetData();
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    },
    async handleUpload() {
      if (!this.container.file) return;
      this.status = Status.uploading;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.container.hash = await this.calculateHash(fileChunkList);

      const { shouldUpload, uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      if (!shouldUpload) {
        this.$message.success("skip upload：file upload success");
        this.status = Status.wait;
        return;
      }

      this.data = fileChunkList.map(({ file }, index) => ({
        fileHash: this.container.hash,
        index,
        hash: this.container.hash + "-" + index,
        chunk: file,
        size: file.size,
        percentage: uploadedList.includes(index) ? 100 : 0
      }));

      await this.uploadChunks(uploadedList);
    },
    // 上传切片，同时过滤已上传的切片
    // upload chunks and filter uploaded chunks
    async uploadChunks(uploadedList = []) {
      const requestList = this.data
        .filter(({ hash }) => !uploadedList.includes(hash))
        .map(({ chunk, hash, index }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          formData.append("fileHash", this.container.hash);
          return { formData, index };
        })
        .map(({ formData, index }) =>
          this.request({
            url: "http://localhost:3000",
            data: formData,
            onProgress: this.createProgressHandler(this.data[index]),
            requestList: this.requestList
          })
        );
      await Promise.all(requestList);
      // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时合并切片
      // merge chunks when the number of chunks uploaded before and
      // the number of chunks uploaded this time
      // are equal to the number of all chunks
      if (uploadedList.length + requestList.length === this.data.length) {
        await this.mergeRequest();
      }
    },
    // 通知服务端合并切片
    // notify server to merge chunks
    async mergeRequest() {
      await this.request({
        url: "http://localhost:3000/merge",
        headers: {
          "content-type": "application/json"
        },
        data: JSON.stringify({
          size: SIZE,
          fileHash: this.container.hash,
          filename: this.container.file.name
        })
      });
      this.$message.success("upload success");
      this.status = Status.wait;
    },
    // 根据 hash 验证文件是否曾经已经被上传过
    // 没有才进行上传
    // verify that the file has been uploaded based on the hash
    // skip if uploaded
    async verifyUpload(filename, fileHash) {
      const { data } = await this.request({
        url: "http://localhost:3000/verify",
        headers: {
          "content-type": "application/json"
        },
        data: JSON.stringify({
          filename,
          fileHash
        })
      });
      return JSON.parse(data);
    },
    // 用闭包保存每个 chunk 的进度数据
    // use closures to save progress data for each chunk
    createProgressHandler(item) {
      return e => {
        const percentage = parseInt(String((e.loaded / e.total) * 100));
        item.percentage =
          item.percentage > percentage ? item.percentage : percentage;
      };
    },

    // 下载上传的文件
    async handleDownLoad() {
      const filename = this.container.file.name;
      const fileHash = this.container.hash;
      const data = await this.request({
        url: "http://localhost:3000/download",
        headers: {
          responseType: "blob",
          "content-type": "application/json"
        },
        data: JSON.stringify({
          filename,
          fileHash
        })
      });
      console.log(data);
      // const blob = new Blob([data]);
      // saveAs(blob, "file.zip");
    }
  }
};
</script>
