软件项目技术点(19)——文件的保存和打开(解压缩)
AxeSlide软件项目梳理 canvas绘图系列知识点整理
保存文件
保存内容有哪些?
我们需要保存的内容信息
1)context.json 存储画布状态信息和所有元素的配置信息(这个文件在过程中生成)
2)插入的图片、音频、视频等资源
3)所用到的字体文件
4)每一帧的缩略图
将这些文件压缩到一个zip包里,我们的作品保存后的文件是dbk后缀格式的,其实就是一个压缩zip文件。
保存过程步骤解析
1)获取要保存的对象信息dtoCore,后面将其转换成字符串string后存储到文件里。
2)将保存作品用到的资源整理到fileList中,fileList对象列表有rawPath(原路径)和target(目标路径)。
3)利用模块zip-addon 将所有的file利用zip.exe压缩到目标路径
require("zip-addon") 可以从github上下载:https://github.com/liufangfang/zip-addon
下面是该模块的主要代码:
var fs = require('fs'); var cp = require('child_process'); var path = require('path'); var platform = process.platform; function zipFiles(listfile){//利用zip.exe根据list.txt文件里的内容来处理压缩文件 var exeName = platform == 'win32' ? 'win/zip.exe' : 'mac/zip.out'; try{ cp.execFileSync( path.join(__dirname, exeName), [listfile], {cwd: process.cwd()} ); return ''; } catch(e){ return String(e.stdout); } } function createDbkFile2(fileList, targetZipPath){ var delimiter = platform == 'win32' ? "|" : ":"; var all = targetZipPath + '\n'; var len = fileList.length; for(var i=0; i<len; i++){//拼接压缩文件内容字符串all var rawPath = String(fileList[i].rawPath); var targetPath = String(fileList[i].targetPath); all += rawPath + delimiter + targetPath + '\n'; } var listFile; if (platform == 'win32') { listFile = path.join(__dirname, 'win/list.txt'); } else { listFile = path.join(__dirname, 'mac/list.txt'); } try { fs.writeFileSync(listFile, all, 'utf8');//将字符串写入list.txt } catch(e) { return e.message; } return zipFiles(listFile); } exports.createDbkFile2 = createDbkFile2;
保存中注意的问题
1)过滤掉重复文件
2)保存失败怎么办,我们这里处理可重试三次
3)一个已存在文件再次保存时失败不应该将已存在的正确文件覆盖掉
针对第一个问题,过滤重复文件的代码如下:
static distinctList(inList) { var outList = [], distinct = {}; var i, len = inList.length; for (i = 0; i < len; i++) { var obj = inList[i]; if (!distinct[obj.targetPath]) { outList.push(obj); distinct[obj.targetPath] = true; } } return outList; }
针对上述第三个问题,我们先保存到path+“.temp”文件,成功后再将源文件删除,将path+“.temp”文件重命名为要保存的文件名
public createDbkFile(path, dtoCore, onSuccess: Function) { var version = ""; var that = this; this.getDtoFileList(dtoCore, true, true, function (bool, fileList) {//fileList要压缩的文件列表 if (!bool) { onSuccess(bool); return; } //将dtoCore对象的clipImage置空,减少context的大小 dtoCore.frames.frames.foreach(function (i, obj) { obj.clipImage = null; }) //净化dtoCore dtoCore.fontsUsed = null; dtoCore.textsUsed = null; var dtoCoreObj = JSON.decycle(dtoCore, true); var packageConfig = JSON.stringify(dtoCoreObj); var dbkTempPath = path + ".temp";//保存文件的临时文件名 Common.FileSytem.createSmallDbk(packageConfig, fileList, dbkTempPath, (e) => {//temp临时文件成功后的回调函数 if (e) { Common.Logger.setErrLog(Common.LogCode.savedbk, "文件:Editor,方法:createDbkFile,异常信息:" + e); onSuccess(false); } else { try { if (Common.FileSytem.existsSync(path)) {//判断是否已经存在该文件 Common.FileSytem.fsExt.removeSync(path); } Common.FileSytem.renameSync(dbkTempPath, path.replace(/\\/g, "/").split("/").pop()); if (fileList.get(0) && (fileList.get(0).targetPath == "cover.png") && !editor.isUploadFile) { index.template.saveLocal(path.replace(/\\/g, "/"), fileList.get(0).rawPath); } if (Common.FileSytem.checkZipIntegrity(path)) {//检查zip文件完整性 onSuccess(true); } else { Common.Logger.setErrLog(Common.LogCode.savedbk, "文件:Editor,方法:createDbkFile(1),异常信息:" + e); onSuccess(false); } } catch (e) { Common.Logger.setErrLog(Common.LogCode.savedbk, "文件:Editor,方法:createDbkFile,异常信息:" + e); onSuccess(false); } } }); }); }
//检查zip文件夹完整性,判断保存的zip文件是否正确 static checkZipIntegrity(file: string, fileCount: number = undefined): boolean { var fd; var buf = new Buffer(22); var fs = Common.FileSytem.fsExt; try { fd = Common.FileSytem.fsExt.openSync(file, "r"); var stat = fs.fstatSync(fd); var len = stat.size; if (len < 22) throw new Error("file size too small"); var n = Common.FileSytem.fsExt.readSync(fd, buf, 0, 22, len-22); if (n != 22) throw new Error("read size error"); //0x50 0x4b 0x05 0x06 if (buf[0] != 0x50 || buf[1] != 0x4b || buf[2] != 0x05 || buf[3] != 0x06) throw new Error("zip Integrity head Error"); var fc = buf[8] | (buf[9] << 8); if (fileCount && (fc != fileCount)) throw new Error("zip Integrity fileCount Error"); Common.FileSytem.fsExt.closeSync(fd); return true; } catch (e) { Common.FileSytem.fsExt.closeSync(fd); return false; } }
打开文件
跟保存过程可以颠倒着来看整个过程,打开的文件是个dbk文件其实就是zip文件
1. 解压文件
我们需要进行解压缩文件 require(unzip)。 https://github.com/EvanOxfeld/node-unzip
//打开dbk static openDbk(filePath, isSetFilePath, callback) { //解压 var unzip = require('unzip'); var that = this; try { var extractTempFolder = FileSytem.tempDbkPath + Util.getGenerate();//创建一个解压目录 FileSytem.mkdirpSync(extractTempFolder); var readStream = this.fs.createReadStream(filePath);//读取zip文件包 var unzipExtractor = unzip.Extract({ path: extractTempFolder }) unzipExtractor.on('error', function (err) { callback(false); }); unzipExtractor.on('close',() => { that.copyExtratedDbkFile(extractTempFolder + "/dbk",(isSuccess) => {//解压完成后,在进行copy过程,将解压出来的资源copy对应的目录下 isSuccess && isSetFilePath && index.template.saveLocal(filePath.replace(/\\/g, "/"), extractTempFolder + "/dbk/cover.png"); callback(isSuccess, extractTempFolder + "/dbk"); }) }); readStream.pipe(unzipExtractor); } catch (e) { callback(false); } }
2. 批量copy过程
解压完成后,再进行copy过程,将解压出来的资源copy对应的目录下。
基于我们的作品包含的文件可能会很多,我们通过模块(lazystream)来实现边读编写的实现复制功能。
https://github.com/jpommerening/node-lazystream
private static copyExtratedDbkFile(sourcePath, callBack) { var lazyStream = require('lazystream'); //获取所有文件列表遍历读写到对应目录 this.DirUtil.Files(sourcePath, 'file',(err, result: Array<String>) => { if (result && result.length > 0) { var steps = 0; function step() { steps++; if (steps >= result.length) { callBack(true); } } result.forEach((value: string, index: number) => { var fileName = value; if (fileName.toLowerCase().indexOf(".ttf") > -1 || fileName.toLowerCase().indexOf(".otf") > -1) { step(); } else if (fileName.toLowerCase().replace(/\\/g, "/").indexOf("/frame/") > -1) {//copy文件为缩略图的话,变更目标地址 var frameName = FileSytem.path.basename(fileName); var readable = new lazyStream.Readable(function () { return FileSytem.fs.createReadStream(fileName) }); var writable = new lazyStream.Writable(() => { return FileSytem.fs.createWriteStream(editor.canvas.canvasImp.framePath + frameName).on('close', function () { step(); }); }); } else { var dest = fileName.replace(/\\/g, "/").replace(sourcePath + "/", 'slideview/'); var readable = new lazyStream.Readable(function () { return FileSytem.fs.createReadStream(fileName) }); var writable = new lazyStream.Writable(() => { return FileSytem.fs.createWriteStream(dest).on('close', function () { step(); }); }); } if (readable) {//读文件流 写文件流 readable.pipe(writable); } }); } }, null); }
3. 获取一个目录下的所有文件
复制的过程,我们是一个一个文件进行读写,在复制之前我们用dirUtil.File获取到了目录下所有文件。
//获取一个目录下的所有子目录,所有文件的方法 export class Dir { fs = require('fs'); path = require('path'); /** * find all files or subdirs (recursive) and pass to callback fn * * @param {string} dir directory in which to recurse files or subdirs * @param {string} type type of dir entry to recurse ('file', 'dir', or 'all', defaults to 'file') * @param {function(error, <Array.<string>)} callback fn to call when done * @example * dir.files(__dirname, function(err, files) { * if (err) throw err; * console.log('files:', files); * }); */ Files(dir, type, callback, /* used internally */ ignoreType) { var that = this; var pending, results = { files: [], dirs: [] }; var done = function () { if (ignoreType || type === 'all') { callback(null, results); } else { callback(null, results[type + 's']); } }; var getStatHandler = function (statPath) { return function (err, stat) { if (err) return callback(err); if (stat && stat.isDirectory() && stat.mode !== 17115) { if (type !== 'file') { results.dirs.push(statPath); } that.Files(statPath, type, function (err, res) { if (err) return callback(err); if (type === 'all') { results.files = results.files.concat(res.files); results.dirs = results.dirs.concat(res.dirs); } else if (type === 'file') { results.files = results.files.concat(res.files); } else { results.dirs = results.dirs.concat(res.dirs); } if (!--pending) done(); }, true); } else { if (type !== 'dir') { results.files.push(statPath); } // should be the last statement in statHandler if (!--pending) done(); } }; }; if (typeof type !== 'string') { ignoreType = callback; callback = type; type = 'file'; } this.fs.stat(dir, function (err, stat) { if (err) return callback(err); if (stat && stat.mode === 17115) return done(); that.fs.readdir(dir, function (err, list) { if (err) return callback(err); pending = list.length; if (!pending) return done(); for (var file, i = 0, l = list.length; i < l; i++) { file = that.path.join(dir, list[i]); that.fs.stat(file, getStatHandler(file)); } }); }); } /** * find all files and subdirs in a directory (recursive) and pass them to callback fn * * @param {string} dir directory in which to recurse files or subdirs * @param {boolean} combine whether to combine both subdirs and filepaths into one array (default false) * @param {function(error, Object.<<Array.<string>, Array.<string>>)} callback fn to call when done * @example * dir.paths(__dirname, function (err, paths) { * if (err) throw err; * console.log('files:', paths.files); * console.log('subdirs:', paths.dirs); * }); * dir.paths(__dirname, true, function (err, paths) { * if (err) throw err; * console.log('paths:', paths); * }); */ Paths(dir, combine, callback) { var type; if (typeof combine === 'function') { callback = combine; combine = false; } this.Files(dir, 'all', function (err, results) { if (err) return callback(err); if (combine) { callback(null, results.files.concat(results.dirs)); } else { callback(null, results); } }, null); } /** * find all subdirs (recursive) of a directory and pass them to callback fn * * @param {string} dir directory in which to find subdirs * @param {string} type type of dir entry to recurse ('file' or 'dir', defaults to 'file') * @param {function(error, <Array.<string>)} callback fn to call when done * @example * dir.subdirs(__dirname, function (err, paths) { * if (err) throw err; * console.log('files:', paths.files); * console.log('subdirs:', paths.dirs); * }); */ Subdirs(dir, callback) { this.Files(dir, 'dir', function (err, subdirs) { if (err) return callback(err); callback(null, subdirs); }, null); } }
- 将子节点的所有父节点ID合并成一个字符串,并更新表
- shell 数组、字典、source、split简单实例
- opencv+人脸识别(识别出来是某个人)
- eclipse中各图标的含义
- Struts 自定义类型转化器
- Unity 3d 常用脚本
- 机器学习算法中GBDT和XGBOOST
- 学生表和成绩表大于60分的信息
- 一种红包发送功能的实现(redis+mysql+quartz)
- 用友nc65 uap开发跳转编辑状态自动赋值参照值无法带出解决办法
- sublime less自动变成css
- Eclipse工程中配置Maven国内镜像仓库
- 类的成员之一属性
- 修改placeholder的样式
- MongoDB 高可用分片配置(Windows、Linux)
- Qt实现FTP下载
- Volatile
- 字符串取 后 n 位
- 链接博客
- 【C++】学习笔记二十五——switch语句