socket.io-file
Version:
File uploader module for Socket.io
327 lines (272 loc) • 8.73 kB
JavaScript
;
const EventEmitter = require('events').EventEmitter;
const util = require('util');
const fs = require('fs');
const path = require('path');
const mime = require('mime');
const mkdirSyncRecursively = require('./utils').mkdirSyncRecursively;
const createDirectoryIfNotExists = require('./utils').createDirectoryIfNotExists;
function SocketIOFile(socket, options) {
if(!socket) {
throw new Error('SocketIOFile requires Socket.');
}
this.options = options || {};
this.maxFileSize = +options.maxFileSize || undefined;
this.accepts = options.accepts || [];
this.chunkSize = +options.chunkSize || 10240;
this.transmissionDelay = options.transmissionDelay || 0;
this.overwrite = !!options.overwrite || false;
this.rename = options.rename || null;
this.resume = !!options.resume || false;
if(!options.uploadDir) {
throw new Error('No upload directory specified.');
}
// check directory is exists
if(typeof options.uploadDir === 'string') {
createDirectoryIfNotExists(options.uploadDir);
}
else if(typeof options.uploadDir === 'object') {
for(var key in options.uploadDir) {
createDirectoryIfNotExists(options.uploadDir[key]);
}
}
else {
throw new Error('options.uploadDir must be string or object array.');
}
this.socket = socket;
socket.on('socket.io-file::reqSync', () => {
socket.emit('socket.io-file::recvSync', {
maxFileSize: this.maxFileSize,
accepts: this.accepts,
chunkSize: this.chunkSize,
transmissionDelay: this.transmissionDelay
});
this.emit('ready');
});
var self = this;
var uploadingFiles = {};
socket.on('socket.io-file::createFile', (fileInfo) => {
var id = fileInfo.id;
var uploadDir = null;
var uploadTo = fileInfo.uploadTo || '';
var data = fileInfo.data || {};
var filename = fileInfo.name;
var originalFileName = fileInfo.name;
function sendError(err) {
socket.emit(`socket.io-file::error::${id}`, {
message: err.message
});
self.emit('error', err, {
uploadId: id,
name: filename,
uploadTo: uploadTo,
data: data
});
}
if(this.rename) {
if(typeof this.rename === 'function') {
filename = this.rename(filename, fileInfo);
}
else {
filename = this.rename;
}
}
if(typeof options.uploadDir === 'string') {
uploadDir = path.join(options.uploadDir, filename);
}
else if(typeof options.uploadDir === 'object') {
if(!uploadTo) {
return sendError(new Error('Upload directory must be specified in multiple directories.'));
}
else if(options.uploadDir[uploadTo]) {
uploadDir = path.join(options.uploadDir[uploadTo], filename);
}
else {
return sendError(new Error('Upload directory ' + uploadTo + ' is not exists.'));
}
}
else if(fileInfo.size > this.maxFileSize) {
return sendError(new Error('Max Uploading File size must be under ' + this.maxFileSize + ' byte(s).'));
}
var startTime = new Date();
const emitObj = {
name: filename,
size: fileInfo.size,
uploadDir: uploadDir,
data: data
};
if(this.rename) {
// rename setting
// add origininalFileName to emitObj
emitObj.originalFileName = originalFileName;
}
this.emit('start', emitObj);
const uploadComplete = () => {
const ws = uploadingFiles[id].writeStream;
if(ws) {
ws.end();
}
const endTime = new Date();
const mimeType = mime.lookup(uploadDir);
const emitObj = {
name: filename,
size: uploadingFiles[id].size,
wrote: uploadingFiles[id].wrote,
uploadDir: uploadingFiles[id].uploadDir,
data: uploadingFiles[id].data,
mime: mimeType,
estimated: endTime - startTime,
uploadId: id
};
if(this.rename) {
// rename setting
// add originalFileName to emitObj
emitObj.originalFileName = originalFileName;
}
if(this.accepts && this.accepts.length > 0) {
let found = false;
for(var i = 0; i < this.accepts.length; i++) {
let accept = this.accepts[i];
if(mimeType === accept) {
found = true;
break;
}
}
// if mime is invalid, remove files and emit error
if(!found) {
fs.unlink(uploadDir); // no after works.
let err = new Error('Not Acceptable file type ' + mimeType + ' of ' + filename + '. Type must be one of these: ' + this.accepts.join(', '));
return sendError(err);
}
else {
self.socket.emit(`socket.io-file::complete::${id}`, emitObj);
self.emit('complete', emitObj);
}
}
else {
self.socket.emit(`socket.io-file::complete::${id}`, emitObj);
self.emit('complete', emitObj);
}
// Release event handlers
socket.removeAllListeners(`socket.io-file::stream::${id}`);
socket.removeAllListeners(`socket.io-file::done::${id}`);
socket.removeAllListeners(`socket.io-file::complete::${id}`);
socket.removeAllListeners(`socket.io-file::abort::${id}`);
socket.removeAllListeners(`socket.io-file::error::${id}`);
delete uploadingFiles[id];
};
uploadingFiles[id] = {
writeStream: null,
name: fileInfo.name,
size: fileInfo.size,
wrote: 0,
uploadDir: uploadDir,
data: data,
resume: false,
};
// check if file exists
const isFileExists = fs.existsSync(uploadDir);
if (isFileExists) {
const uploadedFileStats = fs.statSync(uploadDir);
if (this.resume) {
if (uploadingFiles[id].size > 0) {
if (uploadingFiles[id].size > uploadedFileStats.size) {
uploadingFiles[id].wrote = uploadedFileStats.size;
uploadingFiles[id].resume = true;
socket.emit(`socket.io-file::resume::${id}`, uploadingFiles[id]);
} else {
if (!this.overwrite) return uploadComplete();
}
}
} else {
if (!this.overwrite) return uploadComplete();
}
}
if(!options.overwrite) {
let isFileExists = false;
try {
fs.accessSync(uploadDir, fs.F_OK);
isFileExists = true;
}
catch(e) {
// console.log('File is not exists, so create new one.');
}
if(isFileExists) return uploadComplete();
}
var writeStream = fs.createWriteStream(uploadDir);
uploadingFiles[id].writeStream = writeStream;
socket.emit(`socket.io-file::request::${id}`);
socket.on(`socket.io-file::stream::${id}`, (chunk) => {
if(uploadingFiles[id].abort) {
socket.removeAllListeners(`socket.io-file::stream::${id}`);
socket.removeAllListeners(`socket.io-file::done::${id}`);
socket.removeAllListeners(`socket.io-file::complete::${id}`);
socket.removeAllListeners(`socket.io-file::abort::${id}`);
socket.removeAllListeners(`socket.io-file::error::${id}`);
uploadingFiles[id].writeStream.end();
delete uploadingFiles[id];
return;
}
var writeStream = uploadingFiles[id].writeStream;
function write() {
let result = (uploadingFiles[id].wrote + chunk.length) > (self.maxFileSize);
if( (uploadingFiles[id].wrote + chunk.length) > (self.maxFileSize)) {
return sendError(new Error(`Uploading file size exceeded max file size ${self.maxFileSize} byte(s).`));
}
var writeDone = writeStream.write(chunk);
uploadingFiles[id].wrote += chunk.length;
self.emit('stream', {
name: uploadingFiles[id].name,
size: uploadingFiles[id].size,
wrote: uploadingFiles[id].wrote,
uploadDir: uploadingFiles[id].uploadDir,
data: uploadingFiles[id].data,
uploadId: id
});
if(!writeDone) {
writeStream.once('drain', () => socket.emit(`socket.io-file::request::${id}`));
}
else {
if(self.transmissionDelay) {
setTimeout(() => {
socket.emit(`socket.io-file::request::${id}`);
}, self.transmissionDelay);
}
else {
socket.emit(`socket.io-file::request::${id}`);
}
}
}
write();
});
socket.on(`socket.io-file::done::${id}`, () => {
uploadComplete();
});
socket.on(`socket.io-file::abort::${id}`, () => {
uploadingFiles[id].abort = true;
self.emit('abort', {
name: uploadingFiles[id].name,
size: uploadingFiles[id].size,
wrote: uploadingFiles[id].wrote,
uploadDir: uploadingFiles[id].uploadDir,
data: uploadingFiles[id].data,
uploadId: id
});
socket.emit(`socket.io-file::abort::${id}`, {
name: uploadingFiles[id].name,
size: uploadingFiles[id].size,
wrote: uploadingFiles[id].wrote,
uploadDir: uploadingFiles[id].uploadDir
});
});
});
}
SocketIOFile.prototype.destroy = function() {
this.emit('destroy');
this.socket.emit('socket.io-file::disconnectByServer');
// Clear the resources
this.removeAllListeners();
this.socket = null; // Remove reference of Socket.io
};
util.inherits(SocketIOFile, EventEmitter);
module.exports = SocketIOFile;