FileDecompressor.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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. this.rarPath = '/usr/bin/rar';
  17. this.rarExists = false;
  18. (async() => {
  19. if (await fs.pathExists(this.rarPath))
  20. this.rarExists = true;
  21. })();
  22. }
  23. async decompressNested(filename, outputDir) {
  24. await fs.ensureDir(outputDir);
  25. const fileType = await this.detector.detectFile(filename);
  26. let result = {
  27. sourceFile: filename,
  28. sourceFileType: fileType,
  29. selectedFile: filename,
  30. filesDir: outputDir,
  31. files: []
  32. };
  33. if (!fileType || !(
  34. fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz'
  35. || fileType.ext == 'tar' || (this.rarExists && fileType.ext == 'rar')
  36. )
  37. ) {
  38. return result;
  39. }
  40. result.files = await this.decompressTarZZ(fileType.ext, filename, outputDir);
  41. let sel = filename;
  42. let max = 0;
  43. if (result.files.length) {
  44. //ищем файл с максимальным размером
  45. for (let file of result.files) {
  46. if (file.size > max) {
  47. sel = `${outputDir}/${file.path}`;
  48. max = file.size;
  49. }
  50. }
  51. }
  52. result.selectedFile = sel;
  53. if (sel != filename) {
  54. result.nesting = await this.decompressNested(sel, `${outputDir}/${utils.randomHexString(10)}`);
  55. }
  56. return result;
  57. }
  58. async unpack(filename, outputDir) {
  59. const fileType = await this.detector.detectFile(filename);
  60. if (!fileType)
  61. throw new Error('Не удалось определить формат файла');
  62. return await this.decompress(fileType.ext, filename, outputDir);
  63. }
  64. async unpackTarZZ(filename, outputDir) {
  65. const fileType = await this.detector.detectFile(filename);
  66. if (!fileType)
  67. throw new Error('Не удалось определить формат файла');
  68. return await this.decompressTarZZ(fileType.ext, filename, outputDir);
  69. }
  70. async decompressTarZZ(fileExt, filename, outputDir) {
  71. const files = await this.decompress(fileExt, filename, outputDir);
  72. if (fileExt == 'tar' || files.length != 1)
  73. return files;
  74. const tarFilename = `${outputDir}/${files[0].path}`;
  75. const fileType = await this.detector.detectFile(tarFilename);
  76. if (!fileType || fileType.ext != 'tar')
  77. return files;
  78. const newTarFilename = `${outputDir}/${utils.randomHexString(30)}`;
  79. await fs.rename(tarFilename, newTarFilename);
  80. const tarFiles = await this.decompress('tar', newTarFilename, outputDir);
  81. await fs.remove(newTarFilename);
  82. return tarFiles;
  83. }
  84. async decompress(fileExt, filename, outputDir) {
  85. let files = [];
  86. if (fileExt == 'rar' && this.rarExists) {
  87. files = await this.unRar(filename, outputDir);
  88. return files;
  89. }
  90. switch (fileExt) {
  91. case 'zip':
  92. files = await this.unZip(filename, outputDir);
  93. break;
  94. case 'bz2':
  95. files = await this.unBz2(filename, outputDir);
  96. break;
  97. case 'gz':
  98. files = await this.unGz(filename, outputDir);
  99. break;
  100. case 'tar':
  101. files = await this.unTar(filename, outputDir);
  102. break;
  103. default:
  104. throw new Error(`FileDecompressor: неизвестный формат файла '${fileExt}'`);
  105. }
  106. return files;
  107. }
  108. async unZip(filename, outputDir) {
  109. const zip = new ZipStreamer();
  110. try {
  111. return await zip.unpack(filename, outputDir, {
  112. limitFileSize: this.limitFileSize,
  113. limitFileCount: 10000,
  114. decodeEntryNameCallback: (nameRaw) => {
  115. return utils.bufferRemoveZeroes(nameRaw);
  116. }
  117. });
  118. } catch (e) {
  119. fs.emptyDir(outputDir);
  120. return await zip.unpack(filename, outputDir, {
  121. limitFileSize: this.limitFileSize,
  122. limitFileCount: 10000,
  123. decodeEntryNameCallback: (nameRaw) => {
  124. nameRaw = utils.bufferRemoveZeroes(nameRaw);
  125. const enc = textUtils.getEncodingLite(nameRaw);
  126. if (enc.indexOf('ISO-8859') < 0) {
  127. return iconv.decode(nameRaw, enc);
  128. }
  129. return nameRaw;
  130. }
  131. });
  132. }
  133. }
  134. unBz2(filename, outputDir) {
  135. return this.decompressByStream(unbzip2Stream(), filename, outputDir);
  136. }
  137. unGz(filename, outputDir) {
  138. return this.decompressByStream(zlib.createGunzip(), filename, outputDir);
  139. }
  140. unTar(filename, outputDir) {
  141. return new Promise((resolve, reject) => { (async() => {
  142. const files = [];
  143. if (this.limitFileSize) {
  144. if ((await fs.stat(filename)).size > this.limitFileSize) {
  145. reject(new Error('Файл слишком большой'));
  146. return;
  147. }
  148. }
  149. const tarExtract = tar.extract(outputDir, {
  150. map: (header) => {
  151. files.push({path: header.name, size: header.size});
  152. return header;
  153. }
  154. });
  155. tarExtract.on('finish', () => {
  156. resolve(files);
  157. });
  158. tarExtract.on('error', (err) => {
  159. reject(err);
  160. });
  161. const inputStream = fs.createReadStream(filename);
  162. inputStream.on('error', (err) => {
  163. reject(err);
  164. });
  165. inputStream.pipe(tarExtract);
  166. })().catch(reject); });
  167. }
  168. decompressByStream(stream, filename, outputDir) {
  169. return new Promise((resolve, reject) => { (async() => {
  170. const file = {path: path.parse(filename).name};
  171. let outFilename = `${outputDir}/${file.path}`;
  172. if (await fs.pathExists(outFilename)) {
  173. file.path = `${utils.randomHexString(10)}-${file.path}`;
  174. outFilename = `${outputDir}/${file.path}`;
  175. }
  176. const inputStream = fs.createReadStream(filename);
  177. const outputStream = fs.createWriteStream(outFilename);
  178. outputStream.on('finish', async() => {
  179. try {
  180. file.size = (await fs.stat(outFilename)).size;
  181. } catch (e) {
  182. reject(e);
  183. }
  184. resolve([file]);
  185. });
  186. stream.on('error', reject);
  187. if (this.limitFileSize) {
  188. let readSize = 0;
  189. stream.on('data', (buffer) => {
  190. readSize += buffer.length;
  191. if (readSize > this.limitFileSize)
  192. stream.destroy(new Error('Файл слишком большой'));
  193. });
  194. }
  195. inputStream.on('error', reject);
  196. outputStream.on('error', reject);
  197. inputStream.pipe(stream).pipe(outputStream);
  198. })().catch(reject); });
  199. }
  200. async unRar(filename, outputDir) {
  201. try {
  202. const args = ['x', '-p-', '-y', filename, `${outputDir}`];
  203. const result = await utils.spawnProcess(this.rarPath, {
  204. killAfter: 60,
  205. args
  206. });
  207. if (result.code == 0) {
  208. const files = [];
  209. await utils.findFiles(async(file) => {
  210. const stat = await fs.stat(file);
  211. files.push({path: path.relative(outputDir, file), size: stat.size});
  212. }, outputDir);
  213. return files;
  214. } else {
  215. const error = `${result.code}|FORLOG|, exec: ${this.rarPath}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
  216. throw new Error(`Архиватор Rar завершился с ошибкой: ${error}`);
  217. }
  218. } catch(e) {
  219. if (e.status == 'killed') {
  220. throw new Error('Слишком долгое ожидание архиватора Rar');
  221. } else if (e.status == 'error') {
  222. throw new Error(e.error);
  223. } else {
  224. throw new Error(e);
  225. }
  226. }
  227. }
  228. async gzipBuffer(buf) {
  229. return new Promise((resolve, reject) => {
  230. zlib.gzip(buf, {level: 1}, (err, result) => {
  231. if (err) reject(err);
  232. resolve(result);
  233. });
  234. });
  235. }
  236. async gzipFile(inputFile, outputFile, level = 1) {
  237. return new Promise((resolve, reject) => {
  238. const gzip = zlib.createGzip({level});
  239. const input = fs.createReadStream(inputFile);
  240. const output = fs.createWriteStream(outputFile);
  241. input.pipe(gzip).pipe(output).on('finish', (err) => {
  242. if (err) reject(err);
  243. else resolve();
  244. });
  245. });
  246. }
  247. async gzipFileIfNotExists(filename, outDir, isMaxCompression) {
  248. const hash = await utils.getFileHash(filename, 'sha256', 'hex');
  249. const outFilename = `${outDir}/${hash}`;
  250. if (!await fs.pathExists(outFilename)) {
  251. await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
  252. // переупакуем через некоторое время на максималках, если упаковали плохо
  253. if (!isMaxCompression) {
  254. const filenameCopy = `${filename}.copy`;
  255. await fs.copy(filename, filenameCopy);
  256. (async() => {
  257. await utils.sleep(5000);
  258. const filenameGZ = `${filename}.gz`;
  259. await this.gzipFile(filenameCopy, filenameGZ, 9);
  260. await fs.move(filenameGZ, outFilename, {overwrite: true});
  261. await fs.remove(filenameCopy);
  262. })().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
  263. }
  264. } else {
  265. await utils.touchFile(outFilename);
  266. }
  267. return outFilename;
  268. }
  269. }
  270. module.exports = FileDecompressor;