1 | import io from 'socket.io-client';
|
2 | import DBManager from './dbManager';
|
3 |
|
4 | function worker(self) {
|
5 | const window = self;
|
6 | const indexedDB = self.indexedDB || self.webkitIndexedDB || self.mozIndexedDB || self.OIndexedDB || self.msIndexedDB;
|
7 | const IDBTransaction = self.IDBTransaction || self.webkitIDBTransaction || self.msIDBTransaction;
|
8 | const IDBKeyRange = self.IDBKeyRange || self.webkitIDBKeyRange || self.msIDBKeyRange;
|
9 | const FileReaderSync = self.FileReaderSync;
|
10 |
|
11 | class SubWorker {
|
12 | constructor({ onChange, ...params}, file) {
|
13 | const defaultParams = {
|
14 | chunkSize: 1 * 1024 * 1024,
|
15 | maxConnectionAttempts: 10,
|
16 | fileThrottle: 1000,
|
17 | url: 'ws://localhost:5000/upload',
|
18 | events: {
|
19 | GET_LAST_CHUNK: 'get-last-chunk',
|
20 | SEND_NEXT_CHUNK: 'send-next-chunk',
|
21 | SEND_NEXT_CHUNK_SUCCESS: 'send-next-chunk-successful',
|
22 | SEND_FILE_SUCCESS: 'send-file-successful',
|
23 | CANCEL_UPLOAD: 'cancel-upload',
|
24 | SEND_CHUNK_AGAIN: 'send-chunk-again',
|
25 | ERROR: 'error'
|
26 | }
|
27 | };
|
28 |
|
29 | this.onMessage = onChange;
|
30 | this.socket = null;
|
31 | this.file = file;
|
32 | this.params = {...defaultParams, ...params};
|
33 | this.events = this.params.events;
|
34 | this.fileSize = this.file.data.size;
|
35 | this.chunkSize = Math.min(this.params.chunkSize, this.fileSize);
|
36 | this.maxChunk = (~~(this.fileSize / this.chunkSize)) - 1;
|
37 | this.offset = 0;
|
38 | this.start = 0;
|
39 | this.end = this.chunkSize;
|
40 | this.progress = 0;
|
41 | this.maxConnectionAttempts = this.params.maxConnectionAttempts;
|
42 | this.errorCount = 1;
|
43 | this.throttle = this.params.fileThrottle;
|
44 | this.previousUpdateTime = Date.now();
|
45 | this.isSuspended = false;
|
46 | this.updateFileState(false);
|
47 | this.openSocket();
|
48 | }
|
49 |
|
50 | postMessage = (message) => {
|
51 | this[message.event](message.payload);
|
52 | }
|
53 |
|
54 | pause = () => {
|
55 | this.isSuspended = true;
|
56 | }
|
57 |
|
58 | resume = () => {
|
59 | this.isSuspended = false;
|
60 | this.process();
|
61 | }
|
62 |
|
63 | stop = () => {
|
64 | this.onMessage({
|
65 | data: {
|
66 | payload: this.createFileObject(false),
|
67 | event: 'cancelFileSender'
|
68 | }
|
69 | });
|
70 | this.socket.emit(this.events.CANCEL_UPLOAD, this.file.id);
|
71 | }
|
72 |
|
73 | updateFileState = () => {
|
74 | const now = Date.now();
|
75 |
|
76 | if ((now - this.previousUpdateTime) >= this.throttle) {
|
77 | this.previousUpdateTime = now;
|
78 |
|
79 | this.onMessage({
|
80 | data: {
|
81 | payload: this.createFileObject(false),
|
82 | event: 'onProgress'
|
83 | }
|
84 | });
|
85 | }
|
86 | }
|
87 |
|
88 | closeFileSender = () => {
|
89 | this.onMessage({
|
90 | data: {
|
91 | payload: this.createFileObject(true),
|
92 | event: 'closeFileSender'
|
93 | }
|
94 | });
|
95 | }
|
96 |
|
97 | handleErrorMessage = (error) => {
|
98 | this.onMessage({
|
99 | data: {
|
100 | payload: error,
|
101 | event: 'onError'
|
102 | }
|
103 | });
|
104 | }
|
105 |
|
106 | createFileObject = (status) => {
|
107 | return {
|
108 | fileId: this.file.id,
|
109 | name: this.file.data.name,
|
110 | size: this.file.data.size,
|
111 | passedBytes: this.end,
|
112 | progress: this.progress,
|
113 | currentChunk: this.offset,
|
114 | type: this.file.data.type,
|
115 | isFinal: status
|
116 | };
|
117 | }
|
118 |
|
119 | process = () => {
|
120 | const reader = new FileReaderSync();
|
121 |
|
122 | this.start = this.offset * this.chunkSize;
|
123 | this.end = Math.min(this.fileSize, (this.offset + 1) * this.chunkSize);
|
124 |
|
125 | if (this.fileSize - this.end < this.chunkSize) {
|
126 | this.end = this.fileSize;
|
127 | }
|
128 |
|
129 | const blob = this.slice(this.file.data, this.start, this.end);
|
130 | const dataUrl = reader.readAsArrayBuffer(blob);
|
131 | const final = this.offset === this.maxChunk;
|
132 |
|
133 | const post = {
|
134 | chunk: dataUrl,
|
135 | fileId: this.file.id,
|
136 | chunkNum: this.offset,
|
137 | chunkSize: dataUrl.byteLength,
|
138 | type: this.file.data.type,
|
139 | name: this.file.data.name,
|
140 | isFinal: final
|
141 | };
|
142 |
|
143 | this.socket.emit(this.events.SEND_NEXT_CHUNK, post);
|
144 | this.previousSendTime = Date.now();
|
145 | }
|
146 |
|
147 | slice = (file, start, end) => {
|
148 | const slice = file.mozSlice ||
|
149 | file.webkitSlice ||
|
150 | file.slice;
|
151 |
|
152 | return slice.bind(file)(start, end);
|
153 | }
|
154 |
|
155 | openSocket = () => {
|
156 | this.socket = io(this.params.url, { transports: ['websocket'] });
|
157 |
|
158 | this.socket.on('connect', () => {
|
159 | console.log('Socket ID: ' + this.socket.id + ' CONNECTED');
|
160 |
|
161 | this.socket.emit(this.events.GET_LAST_CHUNK, JSON.stringify({id: this.file.id}));
|
162 | });
|
163 |
|
164 | this.socket.on(this.events.GET_LAST_CHUNK, (data) => {
|
165 | this.offset = data;
|
166 | this.updateFileState();
|
167 | this.process();
|
168 | });
|
169 |
|
170 | this.socket.on(this.events.SEND_NEXT_CHUNK_SUCCESS, (event) => {
|
171 | this.offset += 1;
|
172 | this.progress = (this.offset / this.maxChunk) * 100;
|
173 | this.updateFileState();
|
174 |
|
175 | if (!this.isSuspended) {
|
176 | this.process();
|
177 | }
|
178 | });
|
179 |
|
180 | this.socket.on(this.events.SEND_FILE_SUCCESS, (event) => {
|
181 | this.progress = this.offset > 0 ? (this.offset / this.maxChunk) * 100 : 100;
|
182 | this.closeFileSender();
|
183 | });
|
184 |
|
185 | this.socket.on(this.events.SEND_CHUNK_AGAIN, () => {
|
186 | this.process();
|
187 | });
|
188 |
|
189 | this.socket.on('disconnect', (reason) => {
|
190 | if (reason === 'io server disconnect') {
|
191 | console.log('Connection closed by server');
|
192 | }
|
193 |
|
194 | if (reason === 'transport close') {
|
195 | if (this.errorCount <= this.maxConnectionAttempts) {
|
196 | this.handleErrorMessage({
|
197 | identifier: this.file.id,
|
198 | error: {
|
199 | message: 'disconnect',
|
200 | reason: reason
|
201 | }
|
202 | });
|
203 |
|
204 | console.log(this.errorCount + ' attempts - ' + 'Server Crashed');
|
205 | this.errorCount += 1;
|
206 | this.socket.open();
|
207 | } else {
|
208 | this.socket.disconnect(true);
|
209 | console.log('Maximum reconnection attempts');
|
210 | }
|
211 | }
|
212 |
|
213 | console.log('Connection closed, reason: ' + reason);
|
214 | });
|
215 |
|
216 | this.socket.on('connect_error', (reason) => {
|
217 | console.log(reason);
|
218 | this.handleErrorMessage({
|
219 | identifier: this.file.id,
|
220 | error: {
|
221 | message: 'connect_error',
|
222 | reason: ''
|
223 | }
|
224 | });
|
225 |
|
226 | if (this.errorCount <= this.maxConnectionAttempts) {
|
227 | console.log('Server is not responding or unavailable');
|
228 | this.errorCount += 1;
|
229 | } else {
|
230 | this.socket.disconnect(true);
|
231 | console.log('Maximum reconnection attempts');
|
232 | }
|
233 | });
|
234 |
|
235 | this.socket.on('connect_failed', () => {
|
236 | this.handleErrorMessage({
|
237 | identifier: this.file.id,
|
238 | error: {
|
239 | message: 'connect_failed',
|
240 | reason: ''
|
241 | }
|
242 | });
|
243 |
|
244 | console.log('Connection Failed');
|
245 | });
|
246 |
|
247 | this.socket.on(this.events.ERROR, (error) => {
|
248 | this.handleErrorMessage({
|
249 | identifier: this.file.id,
|
250 | error: error
|
251 | });
|
252 | });
|
253 | }
|
254 | }
|
255 |
|
256 | class WorkersManager {
|
257 | constructor() {
|
258 | this.subWorkers = {};
|
259 | this.DB = new DBManager();
|
260 | this.params = null;
|
261 | this.throttle = 1000;
|
262 | this.previousTime = Date.now();
|
263 | this.filesState = {};
|
264 | this.bindEvents();
|
265 | }
|
266 |
|
267 | bindEvents = () => {
|
268 | window.onmessage = this.onMessage;
|
269 | }
|
270 |
|
271 | postMessage = (data) => {
|
272 | window.postMessage(data);
|
273 | }
|
274 |
|
275 | onMessage = (message) => {
|
276 | this[message.data.event](message.data.payload);
|
277 | }
|
278 |
|
279 | initialize = () => {
|
280 | this.DB.getStorage((rows) => {
|
281 | const ids = rows.map((row) => row.id);
|
282 |
|
283 | rows.forEach((row) => {
|
284 | const id = row.id;
|
285 |
|
286 | this.subWorkers;
|
287 | this.subWorkers[id] = new SubWorker({onChange: this.onMessage, ...this.params}, row);
|
288 | });
|
289 |
|
290 | this.refreshUploadedFiles(ids);
|
291 | });
|
292 | }
|
293 |
|
294 | refreshUploadedFiles = (data) => {
|
295 | this.postMessage({payload: data, event: 'refreshUploadedFiles'});
|
296 | }
|
297 |
|
298 | setFiles = (payload) => {
|
299 | const url = payload.url;
|
300 |
|
301 | payload.files.forEach((file) => {
|
302 | let params = {
|
303 | onChange: this.onMessage,
|
304 | ...this.params
|
305 | };
|
306 |
|
307 | if (url) {
|
308 | params = {...params, url: url};
|
309 | }
|
310 |
|
311 | this.DB.setFile(file);
|
312 | this.subWorkers[file.id] = new SubWorker(params, file);
|
313 | });
|
314 | }
|
315 |
|
316 | pauseUpload = (fileId) => {
|
317 | this.subWorkers[fileId].pause();
|
318 | }
|
319 |
|
320 | resumeUpload = (fileId) => {
|
321 | this.subWorkers[fileId].resume();
|
322 | }
|
323 |
|
324 | stopUpload = (fileId) => {
|
325 | this.subWorkers[fileId].stop();
|
326 | }
|
327 |
|
328 | deleteFile = (file) => {
|
329 | this.DB.delFile(file);
|
330 | }
|
331 |
|
332 | setParams = (params) => {
|
333 | this.params = params;
|
334 | this.throttle = this.params.mainThrottle || this.throttle;
|
335 | this.initialize();
|
336 | }
|
337 |
|
338 | onProgress = (data, force) => {
|
339 | this.filesState[data.fileId] = data;
|
340 | const filesArray = this.arrayFrom(this.filesState);
|
341 | const now = Date.now();
|
342 |
|
343 | if ((now - this.previousTime) >= this.throttle) {
|
344 | this.previousTime = now;
|
345 | this.postMessage({payload: filesArray, event: 'onProgress'});
|
346 | }
|
347 |
|
348 | if (force) {
|
349 | this.postMessage({payload: filesArray, event: 'onProgress'});
|
350 | }
|
351 | }
|
352 |
|
353 | onError = (error) => {
|
354 | this.postMessage({payload: error, event: 'onError'});
|
355 | }
|
356 |
|
357 | closeFileSender = (data) => {
|
358 | this.postMessage({payload: data, event: 'complete'});
|
359 | this.onProgress(data, true);
|
360 | this.deleteFile(data.fileId);
|
361 |
|
362 | delete this.subWorkers[data.fileId];
|
363 | }
|
364 |
|
365 | cancelFileSender = (data) => {
|
366 | delete this.filesState[data.fileId];
|
367 | const filesArray = this.arrayFrom(this.filesState);
|
368 |
|
369 | this.postMessage({payload: filesArray, event: 'onProgress'});
|
370 | this.deleteFile(data.fileId);
|
371 | delete this.subWorkers[data.fileId];
|
372 | }
|
373 |
|
374 | arrayFrom = (obj) => {
|
375 | const array = [];
|
376 |
|
377 | for (let key in obj) {
|
378 | array.push(obj[key]);
|
379 | }
|
380 |
|
381 | return array;
|
382 | }
|
383 | }
|
384 |
|
385 | const Manager = new WorkersManager();
|
386 | }
|
387 |
|
388 | export default worker;
|