学习了解
fs-extra
const fse = require("fs-extra");
配置
定义存放分片的文件夹和存放合成之后的文件夹
const DEAFULT_TEMP_FILE_LOCATION = path.join(__dirname, './upload_file');
const DEAFULT_MERGED_FILE_LOCATION = path.join(__dirname, './merged_file');
const DEFAULT_OPTIONS = {
tempFileLocation: DEAFULT_TEMP_FILE_LOCATION,
mergedFileLocation: DEAFULT_MERGED_FILE_LOCATION,
};
定义一个类FileUploaderServer表示我们的文件上传服务。
class FileUploaderServer {
fileSpliter = '_SPLIT_';
fileUploaderOptions;
constructor(options) {
this.fileUploaderOptions = Object.assign(DEFAULT_OPTIONS, options);
}
getOptions() {
return this.fileUploaderOptions;
}
}
定义fileSpliter作为分片的标志分割符号。
定义fileUploadOptions为我们的配置,以及函数getOptions()表示获取这些配置的方法。
初始化文件分片上传
初始化文件分片上传,实际上就是根据fileName和时间计算一个md5,并新建一个文件夹:
- fileName 文件名
- 返回上传的id
函数名:initFilePartUpload
判断文件夹是否存在,我们使用的是fse.ensureDir(文件夹路径);
这里,我们生成一个临时文件夹来保存图片的分片,这里我们使用fileName+日期的Hash来作为文件夹的名称。
这里我们使用nodeJS的crypto模块来生成 MD5 的Hash。
function calculateMd5(content) {
const hash = crypto.createHash('md5');
return hash.update(content).digest('hex');
}
接下来我们将生成的Hash串作为uploadId返回来。
const uploadId = calculateMd5(`${fileName}-${Date.now()}`);
接下来我们在tempFileLocation下创建一个名字为uploadId的文件夹。
1.首先检查这个文件夹是否存在,如果存在就不需要创建了,直接提示。
我们使用fse.existsSync(文件夹路径)来检查。
const uploadFolderPath = path.join(tempFileLocation, uploadId);
const uploadFolderExist = fse.existsSync(uploadFolderPath);
if (uploadFolderExist) {
throw new exception_1.FolderExistException('found same upload folder, maybe you meet hash collision');
}
2.创建文件夹:
await fse.mkdir(uploadFolderPath);
最后我们返回我们这个文件夹的uploadId
return uploadId;
上传分片
上传分片,实际上是将partFile写入uploadId对应的文件夹中,写入的文件命名格式为partIndex${this.fileSpliter}md5
第一步:获取上传文件的文件夹位置;
const uploadFolderPath = await this.getUploadFolder(uploadId);
我们通过getUploadFolder(uploadId)来实现。
下面是具体函数内容:
显而易见,还是使用我们上边说的方法
fse.ensureDir(文件夹位置)判断存放分片的文件夹是否存在,然后使用fse.existsSync(文件夹位置)来判断当前id的文件夹是否存在,其实也就是判断初始化接口是否调用成功。最后返回需要上传的文件夹的地址。
第二步:我们将上传的分片文件(partFile)的内容进行MD5处理,得到Hash串。(我们对内容进行编码方便我们过滤重复内容)。还是使用上边的方法calculateMd5
const partFileMd5 = util_1.calculateMd5(partFile);
第三步:我们生成当前文件的路径。使用partIndex表示当前分片的编号,fileSpliter我们上边讲了就是一个分隔符,最后是我们上边生成的内容的Hash。
const partFileLocation = path.join(uploadFolderPath, `${partIndex}${this.fileSpliter}${partFileMd5}.part`);
第四步:将文件(partFile)写入到我们上边得到的地址中。这里我们使用的是fse.writeFile()
await fse.writeFile(partFileLocation, partFile);
获取已经上传的分片信息
获取已上传的分片信息,实际上就是读取这个文件夹下面的内容
第一步:获取文件夹的位置getUploadFolder()。
const uploadFolderPath = await this.getUploadFolder(uploadId);
第二步(核心):获取文件列表;
核心方法fse.readdir(dirPath)来获取。然后通过过滤来获取已上传的文件信息列表。
const dirList = await util_1.listDir(uploadFolderPath);
取消文件上传
取消文件上传,硬删除会直接删除文件夹,软删除会给文件夹改个名字
关键点,两个方法:
fse.remove(文件夹路径);直接删除文件夹fse.rename(旧名称, 新名称);修改文件夹名称
合并分片
完成分片上传,实际上就是将所有分片都读到一起,然后进行md5检查,最后存到一个新的路径下。
第一步:获取合并之后的文件存放位置:
const { mergedFileLocation } = this.fileUploaderOptions;
第二步:确保文件夹的存在。
await fse.ensureDir(mergedFileLocation);
第三步:获取上传分片文件的位置。
const uploadFolderPath = await this.getUploadFolder(uploadId);
第四步:获取分片文件列表。
const dirList = await util_1.listDir(uploadFolderPath);
第五步:寻找.part结尾的文件。
const files = dirList.filter((item) => item.path.endsWith('.part'));
第六步:获取文件存放的路径
const mergedFileDirLocation = path.join(合并文件的位置(mergedFileLocation), 文件内容的md5这个是客户端传过来的);
第七步:确保文件夹的存在
await fse.ensureDir(mergedFileDirLocation);
第八步:生成存放文件的路径,这里我们使用客户端传入的fileName
const mergedFilePath = path.join(mergedFileDirLocation, fileName);
第九步:合并文件(核心)
- 获取所有文件的排序和文件(路径)。
files.map((item) => {
const [index] = item.name.replace(/.part$/, '').split(fileSpliter);
return {
index: parseInt(index),
path: item.path,
};
});
- 对文件进行排序。
const sortedFileList = fileList.sort((a, b) => {
return a.index - b.index;
});
- 合并文件。
这里使用第三方的插件multistream
const MultiStream = require("multistream");
创建读取数据流:
const readStreamList = sortedFileList.map((path) => {
return fse.createReadStream(path);
});
将我们读取到的数据流传入到MultiStream实例中
const multiStream = new MultiStream(readStreamList);
创建写入的Stream:
const writeStream = fse.createWriteStream(输出路径);
multiStream调用pipe()方法
multiStream.pipe(writeStream);
最后就是监听:
const fd = fse.openSync(outputPath, 'w+');
multiStream.on('end', () => {
fse.closeSync(fd);
resolve(true);
});
multiStream.on('error', () => {
fse.closeSync(fd);
reject(false);
});
第十步:检查md5
服务端计算fileMd5
function calculateFileMd5(path) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5');
const readStream = fse.createReadStream(path);
readStream.on('error', (err) => {
reject(err);
});
readStream.on('data', (data) => {
hash.update(data);
});
readStream.on('end', function () {
const md5 = hash.digest('hex');
resolve(md5);
});
});
}
比较
if (mergedFileMd5 !== md5) {
throw new exception_1.Md5Exception('md5 checked failed');
}