FileDecompressor.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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. decodeEntryNameCallback: (nameRaw) => {
  101. return utils.bufferRemoveZeroes(nameRaw);
  102. }
  103. }
  104. );
  105. } catch (e) {
  106. fs.emptyDir(outputDir);
  107. return await zip.unpack(filename, outputDir, {
  108. limitFileSize: this.limitFileSize,
  109. limitFileCount: 1000,
  110. decodeEntryNameCallback: (nameRaw) => {
  111. nameRaw = utils.bufferRemoveZeroes(nameRaw);
  112. const enc = textUtils.getEncodingLite(nameRaw);
  113. if (enc.indexOf('ISO-8859') < 0) {
  114. return iconv.decode(nameRaw, enc);
  115. }
  116. return nameRaw;
  117. }
  118. });
  119. }
  120. }
  121. unBz2(filename, outputDir) {
  122. return this.decompressByStream(unbzip2Stream(), filename, outputDir);
  123. }
  124. unGz(filename, outputDir) {
  125. return this.decompressByStream(zlib.createGunzip(), filename, outputDir);
  126. }
  127. unTar(filename, outputDir) {
  128. return new Promise((resolve, reject) => { (async() => {
  129. const files = [];
  130. if (this.limitFileSize) {
  131. if ((await fs.stat(filename)).size > this.limitFileSize) {
  132. reject('Файл слишком большой');
  133. return;
  134. }
  135. }
  136. const tarExtract = tar.extract(outputDir, {
  137. map: (header) => {
  138. files.push({path: header.name, size: header.size});
  139. return header;
  140. }
  141. });
  142. tarExtract.on('finish', () => {
  143. resolve(files);
  144. });
  145. tarExtract.on('error', (err) => {
  146. reject(err);
  147. });
  148. const inputStream = fs.createReadStream(filename);
  149. inputStream.on('error', (err) => {
  150. reject(err);
  151. });
  152. inputStream.pipe(tarExtract);
  153. })().catch(reject); });
  154. }
  155. decompressByStream(stream, filename, outputDir) {
  156. return new Promise((resolve, reject) => { (async() => {
  157. const file = {path: path.parse(filename).name};
  158. let outFilename = `${outputDir}/${file.path}`;
  159. if (await fs.pathExists(outFilename)) {
  160. file.path = `${utils.randomHexString(10)}-${file.path}`;
  161. outFilename = `${outputDir}/${file.path}`;
  162. }
  163. const inputStream = fs.createReadStream(filename);
  164. const outputStream = fs.createWriteStream(outFilename);
  165. outputStream.on('finish', async() => {
  166. try {
  167. file.size = (await fs.stat(outFilename)).size;
  168. } catch (e) {
  169. reject(e);
  170. }
  171. resolve([file]);
  172. });
  173. stream.on('error', reject);
  174. if (this.limitFileSize) {
  175. let readSize = 0;
  176. stream.on('data', (buffer) => {
  177. readSize += buffer.length;
  178. if (readSize > this.limitFileSize)
  179. stream.destroy(new Error('Файл слишком большой'));
  180. });
  181. }
  182. inputStream.on('error', reject);
  183. outputStream.on('error', reject);
  184. inputStream.pipe(stream).pipe(outputStream);
  185. })().catch(reject); });
  186. }
  187. async gzipBuffer(buf) {
  188. return new Promise((resolve, reject) => {
  189. zlib.gzip(buf, {level: 1}, (err, result) => {
  190. if (err) reject(err);
  191. resolve(result);
  192. });
  193. });
  194. }
  195. async gzipFile(inputFile, outputFile, level = 1) {
  196. return new Promise((resolve, reject) => {
  197. const gzip = zlib.createGzip({level});
  198. const input = fs.createReadStream(inputFile);
  199. const output = fs.createWriteStream(outputFile);
  200. input.pipe(gzip).pipe(output).on('finish', (err) => {
  201. if (err) reject(err);
  202. else resolve();
  203. });
  204. });
  205. }
  206. async gzipFileIfNotExists(filename, outDir, isMaxCompression) {
  207. const hash = await utils.getFileHash(filename, 'sha256', 'hex');
  208. const outFilename = `${outDir}/${hash}`;
  209. if (!await fs.pathExists(outFilename)) {
  210. await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
  211. // переупакуем через некоторое время на максималках, если упаковали плохо
  212. if (!isMaxCompression) {
  213. const filenameCopy = `${filename}.copy`;
  214. await fs.copy(filename, filenameCopy);
  215. (async() => {
  216. await utils.sleep(5000);
  217. const filenameGZ = `${filename}.gz`;
  218. await this.gzipFile(filenameCopy, filenameGZ, 9);
  219. await fs.move(filenameGZ, outFilename, {overwrite: true});
  220. await fs.remove(filenameCopy);
  221. })().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
  222. }
  223. } else {
  224. await utils.touchFile(outFilename);
  225. }
  226. return outFilename;
  227. }
  228. }
  229. module.exports = FileDecompressor;