FileDecompressor.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. const fs = require('fs-extra');
  2. const zlib = require('zlib');
  3. const path = require('path');
  4. const unbzip2Stream = require('unbzip2-stream');
  5. const tar = require('tar-fs');
  6. const iconv = require('iconv-lite');
  7. const ZipStreamer = require('./Zip/ZipStreamer');
  8. const appLogger = new (require('./AppLogger'))();//singleton
  9. const FileDetector = require('./FileDetector');
  10. const textUtils = require('./Reader/BookConverter/textUtils');
  11. const utils = require('./utils');
  12. class FileDecompressor {
  13. constructor(limitFileSize = 0) {
  14. this.detector = new FileDetector();
  15. this.limitFileSize = limitFileSize;
  16. }
  17. async decompressNested(filename, outputDir) {
  18. await fs.ensureDir(outputDir);
  19. const fileType = await this.detector.detectFile(filename);
  20. let result = {
  21. sourceFile: filename,
  22. sourceFileType: fileType,
  23. selectedFile: filename,
  24. filesDir: outputDir,
  25. files: []
  26. };
  27. if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) {
  28. return result;
  29. }
  30. result.files = await this.decompressTarZZ(fileType.ext, filename, outputDir);
  31. let sel = filename;
  32. let max = 0;
  33. if (result.files.length) {
  34. //ищем файл с максимальным размером
  35. for (let file of result.files) {
  36. if (file.size > max) {
  37. sel = `${outputDir}/${file.path}`;
  38. max = file.size;
  39. }
  40. }
  41. }
  42. result.selectedFile = sel;
  43. if (sel != filename) {
  44. result.nesting = await this.decompressNested(sel, `${outputDir}/${utils.randomHexString(10)}`);
  45. }
  46. return result;
  47. }
  48. async unpack(filename, outputDir) {
  49. const fileType = await this.detector.detectFile(filename);
  50. if (!fileType)
  51. throw new Error('Не удалось определить формат файла');
  52. return await this.decompress(fileType.ext, filename, outputDir);
  53. }
  54. async unpackTarZZ(filename, outputDir) {
  55. const fileType = await this.detector.detectFile(filename);
  56. if (!fileType)
  57. throw new Error('Не удалось определить формат файла');
  58. return await this.decompressTarZZ(fileType.ext, filename, outputDir);
  59. }
  60. async decompressTarZZ(fileExt, filename, outputDir) {
  61. const files = await this.decompress(fileExt, filename, outputDir);
  62. if (fileExt == 'tar' || files.length != 1)
  63. return files;
  64. const tarFilename = `${outputDir}/${files[0].path}`;
  65. const fileType = await this.detector.detectFile(tarFilename);
  66. if (!fileType || fileType.ext != 'tar')
  67. return files;
  68. const newTarFilename = `${outputDir}/${utils.randomHexString(30)}`;
  69. await fs.rename(tarFilename, newTarFilename);
  70. const tarFiles = await this.decompress('tar', newTarFilename, outputDir);
  71. await fs.remove(newTarFilename);
  72. return tarFiles;
  73. }
  74. async decompress(fileExt, filename, outputDir) {
  75. let files = [];
  76. switch (fileExt) {
  77. case 'zip':
  78. files = await this.unZip(filename, outputDir);
  79. break;
  80. case 'bz2':
  81. files = await this.unBz2(filename, outputDir);
  82. break;
  83. case 'gz':
  84. files = await this.unGz(filename, outputDir);
  85. break;
  86. case 'tar':
  87. files = await this.unTar(filename, outputDir);
  88. break;
  89. default:
  90. throw new Error(`FileDecompressor: неизвестный формат файла '${fileExt}'`);
  91. }
  92. return files;
  93. }
  94. async unZip(filename, outputDir) {
  95. const zip = new ZipStreamer();
  96. try {
  97. return await zip.unpack(filename, outputDir, {
  98. limitFileSize: this.limitFileSize,
  99. limitFileCount: 1000
  100. });
  101. } catch (e) {
  102. fs.emptyDir(outputDir);
  103. return await zip.unpack(filename, outputDir, {
  104. limitFileSize: this.limitFileSize,
  105. limitFileCount: 1000,
  106. decodeEntryNameCallback: (nameRaw) => {
  107. const enc = textUtils.getEncodingLite(nameRaw);
  108. if (enc.indexOf('ISO-8859') < 0) {
  109. return iconv.decode(nameRaw, enc);
  110. }
  111. return nameRaw;
  112. }
  113. });
  114. }
  115. }
  116. unBz2(filename, outputDir) {
  117. return this.decompressByStream(unbzip2Stream(), filename, outputDir);
  118. }
  119. unGz(filename, outputDir) {
  120. return this.decompressByStream(zlib.createGunzip(), filename, outputDir);
  121. }
  122. unTar(filename, outputDir) {
  123. return new Promise((resolve, reject) => { (async() => {
  124. const files = [];
  125. if (this.limitFileSize) {
  126. if ((await fs.stat(filename)).size > this.limitFileSize) {
  127. reject('Файл слишком большой');
  128. return;
  129. }
  130. }
  131. const tarExtract = tar.extract(outputDir, {
  132. map: (header) => {
  133. files.push({path: header.name, size: header.size});
  134. return header;
  135. }
  136. });
  137. tarExtract.on('finish', () => {
  138. resolve(files);
  139. });
  140. tarExtract.on('error', (err) => {
  141. reject(err);
  142. });
  143. const inputStream = fs.createReadStream(filename);
  144. inputStream.on('error', (err) => {
  145. reject(err);
  146. });
  147. inputStream.pipe(tarExtract);
  148. })().catch(reject); });
  149. }
  150. decompressByStream(stream, filename, outputDir) {
  151. return new Promise((resolve, reject) => { (async() => {
  152. const file = {path: path.parse(filename).name};
  153. let outFilename = `${outputDir}/${file.path}`;
  154. if (await fs.pathExists(outFilename)) {
  155. file.path = `${utils.randomHexString(10)}-${file.path}`;
  156. outFilename = `${outputDir}/${file.path}`;
  157. }
  158. const inputStream = fs.createReadStream(filename);
  159. const outputStream = fs.createWriteStream(outFilename);
  160. outputStream.on('finish', async() => {
  161. try {
  162. file.size = (await fs.stat(outFilename)).size;
  163. } catch (e) {
  164. reject(e);
  165. }
  166. resolve([file]);
  167. });
  168. stream.on('error', reject);
  169. if (this.limitFileSize) {
  170. let readSize = 0;
  171. stream.on('data', (buffer) => {
  172. readSize += buffer.length;
  173. if (readSize > this.limitFileSize)
  174. stream.destroy(new Error('Файл слишком большой'));
  175. });
  176. }
  177. inputStream.on('error', reject);
  178. outputStream.on('error', reject);
  179. inputStream.pipe(stream).pipe(outputStream);
  180. })().catch(reject); });
  181. }
  182. async gzipBuffer(buf) {
  183. return new Promise((resolve, reject) => {
  184. zlib.gzip(buf, {level: 1}, (err, result) => {
  185. if (err) reject(err);
  186. resolve(result);
  187. });
  188. });
  189. }
  190. async gzipFile(inputFile, outputFile, level = 1) {
  191. return new Promise((resolve, reject) => {
  192. const gzip = zlib.createGzip({level});
  193. const input = fs.createReadStream(inputFile);
  194. const output = fs.createWriteStream(outputFile);
  195. input.pipe(gzip).pipe(output).on('finish', (err) => {
  196. if (err) reject(err);
  197. else resolve();
  198. });
  199. });
  200. }
  201. async gzipFileIfNotExists(filename, outDir, isMaxCompression) {
  202. const hash = await utils.getFileHash(filename, 'sha256', 'hex');
  203. const outFilename = `${outDir}/${hash}`;
  204. if (!await fs.pathExists(outFilename)) {
  205. await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
  206. // переупакуем через некоторое время на максималках, если упаковали плохо
  207. if (!isMaxCompression) {
  208. const filenameCopy = `${filename}.copy`;
  209. await fs.copy(filename, filenameCopy);
  210. (async() => {
  211. await utils.sleep(5000);
  212. const filenameGZ = `${filename}.gz`;
  213. await this.gzipFile(filenameCopy, filenameGZ, 9);
  214. await fs.move(filenameGZ, outFilename, {overwrite: true});
  215. await fs.remove(filenameCopy);
  216. })().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
  217. }
  218. } else {
  219. await utils.touchFile(outFilename);
  220. }
  221. return outFilename;
  222. }
  223. }
  224. module.exports = FileDecompressor;