UNPKG

@chunk-master/core

Version:

Core library for chunk-based file upload with resumable functionality

300 lines (299 loc) 8.18 kB
import { inject as N, requestExtender as S } from "@net-vert/core"; import z from "lodash.throttle"; import { reactive as w } from "vue"; const D = { maxSize: 5 * 1024 * 1024, chunkSize: 5 * 1024 * 1024, multiThread: !0, uploadMethod: "post" }, U = { parallelCount: 3, retries: 0 }, A = (s) => { const { parallelCount: t, retries: e } = s, { requestor: r, concurrentPool: a } = S.concurrentPoolRequestor({ ...U, parallelCount: t, retries: e }); return { pool: a, requestor: r }; }, y = (s) => { const { request: t } = s; N(t); }, R = (s) => { const t = new FormData(); for (const e in s) t.append(e, s[e]); return t; }; function G(s, t) { const e = { ...s }; return Object.entries(t).forEach(([r, a]) => { a != null && (e[r] = a); }), e; } const _ = async (s, t, e) => { const r = t * e, a = Math.min(r + e, s.size), h = s.slice(r, a), l = i.hashApi && i.hashcalculation ? await i.hashcalculation(h) : void 0; return { start: r, end: a, index: t, chunk: h, size: a - r, hash: l }; }, b = async (s, t) => { const { file: e, start: r, end: a, chunkSize: h } = s, l = []; try { for (let n = r; n < a; n++) { const u = await _(e, n, h); l.push(u), t(u, n); } return l; } catch (n) { throw console.error("Error creating chunks:", n), n; } }; let i; const $ = (s) => { y(s); const { pool: t, requestor: e } = A(s), { pool: r, parallelCount: a, retries: h, request: l, ...n } = s; return i = { ...G(D, n), sliceFile: s.sliceFile || b, requestor: e, pool: t }, { requestor: e }; }; var o = /* @__PURE__ */ ((s) => (s.PREPARING = "preparing", s.PREPARE_FAILED = "prepare_failed", s.PENDING = "pending", s.PAUSE = "pause", s.UPLOADING = "uploading", s.SUCCESS = "success", s.FAIL = "fail", s))(o || {}), c = /* @__PURE__ */ ((s) => (s.PENDING = "pending", s.SUCCESS = "success", s.FAIL = "fail", s))(c || {}); const P = { total: 0, loaded: 0, progress: 0 }; class F { // 构造函数 constructor(t, e) { this.metadata = t, this.uploader = e, this.controller = new AbortController(), this.responseInterval = 1e3, this.status = c.PENDING, this.promise = new Promise(() => { }), this.updateProgressThrottle = z(this.updateProgress, this.responseInterval), this.metadata = t, this.metadata.hash ? this.id = this.metadata.hash : this.id = `${Date.now()}_${Math.random().toString().slice(2, 8)}_${this.metadata.index}`, this.progressInfo = { ...P, total: this.metadata.chunk.size }; } // 发送请求的方法 async request() { const t = i.beforeUpload ? await i.beforeUpload(this.metadata, this.uploader) : R(this.metadata); return i.requestor.request( { url: i.uploadUrl, method: i.uploadMethod, data: t, signal: this.controller.signal, onUploadProgress: this.updateProgressThrottle.bind(this) } ); } // 执行任务,添加到并发控制池中 execute() { return this.promise = this.request.call(this), this.promise.then((t) => (this.status = c.SUCCESS, t)).catch((t) => (this.status = c.FAIL, t)); } // 取消任务,根据不同状态进行相应处理 cancel() { i.pool.remove(this.id), this.controller.abort(); } // 暂停任务:取消当前任务、重置状态和控制器 pause() { this.cancel(), this.controller = new AbortController(), this.updateProgressThrottle.cancel(), this.progressInfo = { ...P, total: this.metadata.chunk.size }; } updateProgress(t) { const { loaded: e, total: r } = t; this.progressInfo = { loaded: e, total: r, progress: e / r }; } } class L { constructor(t) { this.file = t, this._status = o.PREPARING, this.tasks = [], this.fileInfo = null, i.prepareFileInfo ? i.prepareFileInfo(this.file).then((e) => { this.fileInfo = e, this._status = o.PENDING; }).catch((e) => { throw this._status = o.PREPARE_FAILED, e; }) : this._status = o.PENDING; } async start() { try { if (this._status = o.UPLOADING, this.tasks.length) { const [t] = this.tasks; await t.execute(); } else await this.uploadFile(); this._status = o.SUCCESS; } catch (t) { if (this._status = o.PAUSE) return t; throw t; } } async uploadFile() { const t = w( new F({ index: 0, start: 0, end: this.file.size, size: this.file.size, chunk: this.file }, this) ); this.tasks.push(t), await t.execute(); } pause() { const [t] = this.tasks; this._status = o.PAUSE, t.pause(); } cancel() { const [t] = this.tasks; t.cancel(), this.tasks = []; } get progressInfo() { const [t] = this.tasks; return t ? t.progressInfo : { ...P, total: this.file.size }; } get status() { return this._status; } } const g = (navigator == null ? void 0 : navigator.hardwareConcurrency) || 4, x = async (s, t) => { const { file: e, start: r, end: a, chunkSize: h } = s; return new Promise((l, n) => { const u = Math.ceil((a - r) / g); if (u === 0) l([]); else { const E = []; let C = 0; for (let f = 0; f < g; f++) { const k = r + f * u, I = Math.min(k + u, a); if (k >= I) break; const p = new Worker(new URL("./worker.ts", __dirname), { type: "module" }); p.postMessage({ file: e, start: k, end: I, chunkSize: h }), p.onmessage = (m) => { m.data.forEach((d) => { E[d.index] = d, t(d, d.index); }), p.terminate(), C++, C === Math.min(g, a - r) && l(E); }, p.onerror = (m) => { p.terminate(), n(m); }; } } }); }, q = async (s, t, e, r) => { const a = { file: s, start: t, end: e, chunkSize: i.chunkSize }; return i.hashApi && i.hashcalculation && i.multiThread ? x(a, r) : i.sliceFile(a, r); }; class M { constructor(t) { this.file = t, this._status = o.PREPARING, this.tasks = [], this.fileInfo = null, this.totalChunks = Math.ceil(this.file.size / i.chunkSize), i.prepareFileInfo ? i.prepareFileInfo(this.file).then((e) => { this.fileInfo = e, this._status = o.PENDING; }).catch((e) => { throw this._status = o.PREPARE_FAILED, e; }) : this._status = o.PENDING; } // 这里可以实现大文件的分片上传逻辑 async start() { try { this._status = o.UPLOADING; const t = await this.uploadFile(); this.mergeChunks(t), this._status = o.SUCCESS; } catch (t) { if (this._status = o.PAUSE) return t; throw t; } } async preupload() { const t = await _(this.file, 0, i.chunkSize); this.setTask(t, 0); } async uploadFile() { let t = this.tasks.length; this.tasks.length === 0 ? (this.preupload(), t++) : this.uploadChunks(), await q(this.file, t, this.totalChunks, this.setTask.bind(this)); const e = this.tasks.map((r) => r.promise); return Promise.all(e); } uploadChunks() { for (const t of this.tasks) t.status !== c.SUCCESS && t.execute(); } setTask(t, e) { const r = w(new F(t, this)); this.tasks[e] = r, this.status === o.UPLOADING && r.execute(); } mergeChunks(t) { const e = i.mergeApi; return e( t, this ); } pause() { this._status = o.PAUSE, this.tasks.forEach((t) => { t.status !== c.SUCCESS && t.pause(); }); } cancel() { this.tasks.forEach((t) => { t.status !== c.SUCCESS && t.cancel(); }), this.tasks = []; } get progressInfo() { const t = this.tasks.reduce((r, a) => { const h = a.progressInfo; return r + h.loaded; }, 0), e = Math.min(t, this.file.size) / this.file.size || 0; return { total: this.file.size, loaded: t, progress: e }; } get status() { return this._status; } } function B(s) { return s.size > i.maxSize ? new M(s) : new L(s); } export { o as STATUS, $ as config, B as createFileUploader };