UNPKG

@jianghujs/jianghu

Version:

Progressive Enterprise Framework

345 lines (319 loc) 12.4 kB
'use strict'; // ========================================常用 require start=========================================== const Service = require('egg').Service; const stream = require('stream'); const { BizError, errorInfoEnum } = require('../constant/error'); const validateUtil = require('../common/validateUtil'); const _ = require('lodash'); const mime = require('mime'); // ========================================常用 require end============================================= const path = require('path'); const crypto = require('crypto'); const fileUtil = require('../common/fileUtil'); const actionDataScheme = Object.freeze({ getChunkInfo: { type: 'object', additionalProperties: true, required: [ 'downloadPath' ], properties: { downloadPath: { anyOf: [{ type: 'string' }, { type: 'number' }] }, }, }, uploadFileDone: { type: 'object', additionalProperties: true, required: [ 'hash', 'total', 'chunkSize', 'filename', 'fileDirectory' ], properties: { hash: { anyOf: [{ type: 'string' }, { type: 'number' }] }, total: { anyOf: [{ type: 'string' }, { type: 'number' }] }, chunkSize: { anyOf: [{ type: 'string' }, { type: 'number' }] }, filename: { anyOf: [{ type: 'string' }, { type: 'number' }] }, fileDirectory: { type: 'string' }, fileDesc: { anyOf: [{ type: 'string' }, { type: 'null' }] }, }, }, uploadFileChunkByStream: { type: 'object', additionalProperties: true, required: [ 'hash', 'indexString', 'filename' ], properties: { hash: { anyOf: [{ type: 'string' }, { type: 'number' }] }, indexString: { anyOf: [{ type: 'string' }, { type: 'number' }] }, total: { anyOf: [{ type: 'string' }, { type: 'number' }] }, filename: { anyOf: [{ type: 'string' }, { type: 'number' }] }, chunkSize: { anyOf: [{ type: 'string' }, { type: 'number' }] }, }, }, uploadFileChunkByBase64: { type: 'object', additionalProperties: true, required: [ 'hash', 'indexString', 'filename', 'fileBase64' ], properties: { hash: { anyOf: [{ type: 'string' }, { type: 'number' }] }, indexString: { anyOf: [{ type: 'string' }, { type: 'number' }] }, total: { anyOf: [{ type: 'string' }, { type: 'number' }] }, filename: { anyOf: [{ type: 'string' }, { type: 'number' }] }, chunkSize: { anyOf: [{ type: 'string' }, { type: 'number' }] }, fileBase64: { anyOf: [{ type: 'string' }, { type: 'number' }] }, }, }, uploadFileChunkByBuffer: { type: 'object', additionalProperties: true, required: [ 'hash', 'indexString', 'filename', 'fileBuffer' ], properties: { hash: { anyOf: [{ type: 'string' }, { type: 'number' }] }, indexString: { anyOf: [{ type: 'string' }, { type: 'number' }] }, total: { anyOf: [{ type: 'string' }, { type: 'number' }] }, filename: { anyOf: [{ type: 'string' }, { type: 'number' }] }, chunkSize: { anyOf: [{ type: 'string' }, { type: 'number' }] }, fileBuffer: { anyOf: [{ type: 'object' }] }, }, }, downloadFileChunkByBase64: { type: 'object', additionalProperties: true, required: [ 'downloadPath', 'index' ], properties: { downloadPath: { anyOf: [{ type: 'string' }, { type: 'number' }] }, index: { type: 'number' }, }, }, downloadFileChunkByBuffer: { type: 'object', additionalProperties: true, required: [ 'downloadPath', 'index' ], properties: { downloadPath: { anyOf: [{ type: 'string' }, { type: 'number' }] }, index: { type: 'number' }, }, }, }); class FileService extends Service { async getChunkInfo() { const app = this.app; const { config } = app; const { uploadDir } = config; const { appData } = this.ctx.request.body; const { actionData } = appData; validateUtil.validate(actionDataScheme.getChunkInfo, actionData); const { downloadPath } = actionData; const filePath = path.join(uploadDir, downloadPath); const isFileExists = await fileUtil.exists(filePath); if (!isFileExists) { throw new BizError(errorInfoEnum.file_not_found); } const KB = 1024; const MB = 1024 * KB; const fileStates = await fileUtil.stat(filePath); const fileSize = fileStates.size; let chunkSize = 3 * MB; if (fileSize > 100 * MB) { chunkSize = 5 * MB; } else if (fileSize > 500 * MB) { chunkSize = 8 * MB; } const total = Math.ceil(fileSize / chunkSize); const buffer = await fileUtil.readFile(filePath); const fsHash = crypto.createHash('md5'); fsHash.update(buffer); const hash = fsHash.digest('hex'); return { hash, total, chunkSize, fileSize }; } async uploadFileDone() { const app = this.app; const { userId } = this.ctx.userInfo; const { jianghuKnex, config } = app; const { uploadDir, downloadBasePath } = config; const { tmpdir } = config.multipart; const actionData = this.ctx.request.body.appData.actionData; validateUtil.validate(actionDataScheme.uploadFileDone, actionData); const { hash, filename, total, chunkSize, fileDirectory, fileDesc, } = actionData; let filenameStorage = actionData.filenameStorage; const fileId = `${Date.now()}_${_.random(100000, 999999)}`; if (!filenameStorage) { filenameStorage = `${fileId}_${filename}`; } filenameStorage = filenameStorage.replace(/%/g, ''); const fileUploadPath = path.join(uploadDir, fileDirectory); const filePath = path.join(fileUploadPath, filenameStorage); const isFileExists = await fileUtil.exists(fileUploadPath); if (!isFileExists) { await fileUtil.checkAndPrepareFilePath(fileUploadPath); } // 读取所有分片文件 const chunksPath = path.join(tmpdir, userId, hash); const chunks = await fileUtil.readdir(chunksPath); const chunksPathList = []; if (chunks.length !== total || chunks.length === 0) { await fileUtil.deleteFileAndDirByPath(chunksPath); throw new BizError(errorInfoEnum.file_is_incomplete); } chunks.forEach(item => { chunksPathList.push(path.join(chunksPath, item)); }); // 将分片文件 merge 成一个文件 if (chunks.length === 1) { await fileUtil.copyFile(chunksPathList[0], filePath); await fileUtil.unlink(chunksPathList[0]); } else { await fileUtil.streamMerge(chunksPathList, filePath, chunkSize); } await fileUtil.deleteFileAndDirByPath(chunksPath); // check md5是否一致 const buffer = await fileUtil.readFile(filePath); const fsHash = crypto.createHash('md5'); fsHash.update(buffer); const fileHash = fsHash.digest('hex'); if (hash !== fileHash) { throw new BizError(errorInfoEnum.file_damaged); } const downloadPath = `/${fileDirectory}/${filenameStorage}`; const fileStates = await fileUtil.stat(filePath); // const binarySize = fileUtil.formatByteSize(fileStates.size); // 文件大小/KB const binarySize = (fileStates.size / 1024).toFixed(2); const fileType = mime.getType(filename); const file = { fileId, fileDirectory, filename, fileDesc, filenameStorage, downloadPath, binarySize, fileType, }; file.downloadBasePath = downloadBasePath; file.downloadTip = 'https://xxx.xxx.xxx/${downloadBasePath}${downloadPath}'; return file; } async uploadFileChunkByStream() { const { userId } = this.ctx.userInfo; const { config } = this.app; const { tmpdir } = config.multipart; const actionData = this.ctx.request.body.appData.actionData; validateUtil.validate(actionDataScheme.uploadFileChunkByStream, actionData); const { hash, indexString } = actionData; const chunksPath = path.join(tmpdir, userId, hash); const filepath = path.join(chunksPath, hash + '_' + indexString); const isFileExists = await fileUtil.exists(chunksPath); if (!isFileExists) { await fileUtil.checkAndPrepareFilePath(chunksPath); } if (!this.ctx.request.files || !this.ctx.request.files[0]) { throw new BizError(errorInfoEnum.request_data_invalid); } const file = this.ctx.request.files[0]; await fileUtil.copyFile(file.filepath, filepath); fileUtil.unlink(file.filepath); } async uploadFileChunkByBase64() { const { userId } = this.ctx.userInfo; const { config } = this.app; const { tmpdir } = config.multipart; const actionData = this.ctx.request.body.appData.actionData; validateUtil.validate(actionDataScheme.uploadFileChunkByBase64, actionData); const { hash, indexString, fileBase64 } = actionData; const chunksPath = path.join(tmpdir, userId, hash); const filepath = path.join(chunksPath, hash + '_' + indexString); const isFileExists = await fileUtil.exists(chunksPath); if (!isFileExists) { await fileUtil.checkAndPrepareFilePath(chunksPath); } if (!fileBase64) { throw new BizError(errorInfoEnum.request_data_invalid); } if (_.isEmpty(fileBase64)) { throw new BizError(errorInfoEnum.file_buffer_is_null); } const buffer = fileUtil.base64ToBlob(fileBase64); await fileUtil.writeFile(filepath, buffer); this.ctx.request.body.appData.actionData.fileBase64 = null; } async uploadFileChunkByBuffer() { const { userId } = this.ctx.userInfo; const { config } = this.app; const { tmpdir } = config.multipart; const actionData = this.ctx.request.body.appData.actionData; validateUtil.validate(actionDataScheme.uploadFileChunkByBuffer, actionData); const { hash, indexString, fileBuffer } = actionData; const chunksPath = path.join(tmpdir, userId, hash); const filepath = path.join(chunksPath, hash + '_' + indexString); const isFileExists = await fileUtil.exists(chunksPath); if (!isFileExists) { await fileUtil.checkAndPrepareFilePath(chunksPath); } if (!fileBuffer) { throw new BizError(errorInfoEnum.request_data_invalid); } if (_.isEmpty(fileBuffer)) { throw new BizError(errorInfoEnum.file_buffer_is_null); } // 创建一个bufferstream const bufferStream = new stream.PassThrough(); // 将Buffer写入 bufferStream.end(fileBuffer); // 进一步使用 bufferStream.pipe(process.stdout); await fileUtil.writeFile(filepath, fileBuffer, 'binary'); this.ctx.request.body.appData.actionData.fileBuffer = null; } async downloadFileChunkByBase64() { const app = this.app; const { config } = app; const { uploadDir } = config; const { actionData } = this.ctx.request.body.appData; validateUtil.validate( actionDataScheme.downloadFileChunkByBase64, actionData ); const { total, index, chunkSize, hash, downloadPath } = actionData; const filePath = path.join(uploadDir, downloadPath); const isFileExists = await fileUtil.exists(filePath); if (!isFileExists) { throw new BizError(errorInfoEnum.file_not_found); } const buffer = await fileUtil.readFile(filePath); const currentBuffer = buffer.slice( index * chunkSize, (index + 1) * chunkSize ); let fileBase64 = currentBuffer.toString('base64'); if (index === 0) { const mimeType = mime.getType(filePath); fileBase64 = `data:${mimeType};base64,${fileBase64}`; } return { fileBase64 }; } async downloadFileChunkByBuffer() { const app = this.app; const { config } = app; const { uploadDir } = config; const { actionData } = this.ctx.request.body.appData; validateUtil.validate( actionDataScheme.downloadFileChunkByBuffer, actionData ); const { fileSize, total, index, chunkSize, hash, downloadPath } = actionData; const filePath = path.join(uploadDir, downloadPath); const isFileExists = await fileUtil.exists(filePath); if (!isFileExists) { throw new BizError(errorInfoEnum.file_not_found); } const fd = await fileUtil.open(filePath); let length = chunkSize; if ((index + 1) === total) { length = fileSize - index * chunkSize; } const currentBuffer = Buffer.alloc(length); await fileUtil.read(fd, currentBuffer, 0, length, index * chunkSize); return { fileBuffer: currentBuffer, index }; } } module.exports = FileService;