diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9611171 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.js] +quote_type = single diff --git a/cli.js b/cli.js index 8c604f6..986df7a 100755 --- a/cli.js +++ b/cli.js @@ -7,7 +7,7 @@ const cliProgress = require('cli-progress'); const wpExtract = require('./lib/wpress-extract'); -async function main({ inputFile, outputDir, override }) { +async function main({ inputFile, outputDir, override, ignoreWriteErrors }) { const progressBar = new cliProgress.SingleBar( { format: 'Progress: {bar} | {percentage}%', @@ -29,9 +29,11 @@ async function main({ inputFile, outputDir, override }) { }; let totalFiles = 0; + let errorFiles = 0; let success = false; - const onFinish = (_totalFiles) => { - totalFiles = _totalFiles; + const onFinish = (counts) => { + totalFiles = counts.success; + errorFiles = counts.error; success = true; // Set the progress bar to 100% and stop progressBar.update(progressBar.getTotal()); @@ -44,6 +46,8 @@ async function main({ inputFile, outputDir, override }) { onStart, onUpdate, onFinish, + override, + ignoreWriteErrors, }); } catch (error) { progressBar.stop(); @@ -54,6 +58,11 @@ async function main({ inputFile, outputDir, override }) { if (success) { console.log(); console.log(`Successfully extracted ${totalFiles} files.`); + if (errorFiles > 0) { + console.error( + `${errorFiles} files were not extracted because of write error` + ); + } } } } @@ -74,13 +83,19 @@ yargs(hideBin(process.argv)).command( }) .option('f', { alias: 'force', - describe: 'override existing directory', + describe: 'Override existing directory', + type: 'boolean', + }) + .option('iwe', { + alias: 'ignore-write-errors', + describe: 'Ignore file write errors and continue', type: 'boolean', }); }, (argv) => { const override = !!argv.force; const inputFile = path.resolve(process.cwd(), argv.input); + const ignoreWriteErrors = !!argv.ignoreWriteErrors; let outputDir = typeof argv.out === 'string' @@ -93,6 +108,6 @@ yargs(hideBin(process.argv)).command( outputDir = path.join(process.cwd(), dirName); } - return main({ inputFile, outputDir, override }); + return main({ inputFile, outputDir, override, ignoreWriteErrors }); } ).argv; diff --git a/lib/wpress-extract.js b/lib/wpress-extract.js index 1a33666..4a5b8f4 100644 --- a/lib/wpress-extract.js +++ b/lib/wpress-extract.js @@ -5,6 +5,16 @@ const path = require('path'); const HEADER_SIZE = 4377; // length of the header const HEADER_CHUNK_EOF = Buffer.alloc(HEADER_SIZE); // Empty header used for check if we reached the end +const IS_WIN = process.platform === 'win32'; + +function convertToValidWindowsFilename(input) { + return IS_WIN ? input.replace(/[|\\:*?"<>]/g, '') : input; +} + +function convertToValidWindowsFilepath(input) { + return IS_WIN ? input.replace(/[|\\:*?"<>]/g, '') : input; +} + function isDirEmpty(dirname) { return fs.promises.readdir(dirname).then((files) => { return files.length === 0; @@ -39,10 +49,22 @@ async function readHeader(fd) { }; } -async function readBlockToFile(fd, header, outputPath) { - const outputFilePath = path.join(outputPath, header.prefix, header.name); +async function readBlockToFile(fd, header, outputPath, ignoreWriteErrors) { + const outputFilePath = path.join( + outputPath, + convertToValidWindowsFilepath(header.prefix), + convertToValidWindowsFilename(header.name) + ); fse.ensureDirSync(path.dirname(outputFilePath)); + const outputStream = fs.createWriteStream(outputFilePath); + let writeError = null; + if (ignoreWriteErrors) { + outputStream.on('error', (err) => { + console.error(outputFilePath, err); + writeError = err; + }); + } let totalBytesToRead = header.size; while (true) { @@ -62,7 +84,13 @@ async function readBlockToFile(fd, header, outputPath) { totalBytesToRead -= data.bytesRead; } - outputStream.close(); + return ignoreWriteErrors + ? new Promise((done, reject) => + outputStream.close((err) => + writeError ?? err ? reject(writeError ?? err) : done() + ) + ) + : outputStream.close(); } module.exports = async function wpExtract({ @@ -72,6 +100,7 @@ module.exports = async function wpExtract({ onUpdate, onFinish, override, + ignoreWriteErrors, }) { if (!fs.existsSync(_inputFile)) { throw new Error( @@ -82,12 +111,10 @@ module.exports = async function wpExtract({ if (override) { // Ensure the output dir exists and is empty fse.emptyDirSync(outputDir); - } else { - if (fs.existsSync(outputDir) && !(await isDirEmpty(outputDir))) { - throw new Error( - `Output dir is not empty. Clear it first or use the --force option to override it.` - ); - } + } else if (fs.existsSync(outputDir) && !(await isDirEmpty(outputDir))) { + throw new Error( + `Output dir is not empty. Clear it first or use the --force option to override it.` + ); } const inputFileStat = fs.statSync(_inputFile); @@ -97,7 +124,7 @@ module.exports = async function wpExtract({ onStart(inputFileStat.size); let offset = 0; - let countFiles = 0; + const counts = { success: 0, error: 0 }; while (true) { const header = await readHeader(inputFile); @@ -105,9 +132,17 @@ module.exports = async function wpExtract({ break; } - await readBlockToFile(inputFile, header, outputDir); - offset = offset + HEADER_SIZE + header.size; - countFiles++; + try { + await readBlockToFile(inputFile, header, outputDir, ignoreWriteErrors); + counts.success++; + } catch (err) { + if (!ignoreWriteErrors) { + throw err; + } + console.error(err); + counts.error++; + } + offset += HEADER_SIZE + header.size; // Trigger onUpdate callback onUpdate(offset); @@ -116,5 +151,5 @@ module.exports = async function wpExtract({ await inputFile.close(); // Trigger onFinish callback - onFinish(countFiles); + onFinish(counts); };