-
Notifications
You must be signed in to change notification settings - Fork 35
feat: support xz #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: support xz #112
Changes from all commits
b104f8f
58055ea
9b17e98
d80454e
ee421a6
e8465b5
4f0f447
8d9380f
b167355
b3cb500
d6a07f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
'use strict'; | ||
|
||
const fs = require('fs'); | ||
const lzma = require('lzma-native'); | ||
const utils = require('../utils'); | ||
const streamifier = require('streamifier'); | ||
const stream = require('stream'); | ||
|
||
class XzFileStream extends stream.Transform { | ||
constructor(opts) { | ||
opts = opts || {}; | ||
super(opts); | ||
|
||
const sourceType = utils.sourceType(opts.source); | ||
const compressor = lzma.createCompressor(opts.lzma); | ||
|
||
compressor.on('error', err => this.emit('error', err)); | ||
compressor.on('end', () => this.push(null)); | ||
compressor.on('data', chunk => this.push(chunk)); | ||
|
||
if (sourceType === 'file') { | ||
const stream = fs.createReadStream(opts.source, opts.fs); | ||
stream.on('error', err => this.emit('error', err)); | ||
stream.pipe(compressor); | ||
return; | ||
} | ||
|
||
if (sourceType === 'buffer') { | ||
const stream = streamifier.createReadStream(opts.source, opts.streamifier); | ||
stream.on('error', err => this.emit('error', err)); | ||
stream.pipe(compressor); | ||
return; | ||
} | ||
|
||
if (sourceType === 'stream') { | ||
opts.source.on('error', err => this.emit('error', err)); | ||
opts.source.pipe(compressor); | ||
return; | ||
} | ||
|
||
// For streaming input | ||
this.on('pipe', srcStream => { | ||
srcStream.unpipe(this); | ||
srcStream.pipe(compressor); | ||
}); | ||
} | ||
|
||
_transform(chunk, encoding, callback) { | ||
// This will be handled by the compressor stream | ||
callback(); | ||
} | ||
Comment on lines
+42
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
} | ||
|
||
module.exports = XzFileStream; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
'use strict'; | ||
|
||
const utils = require('../utils'); | ||
|
||
let XzFileStream; | ||
let XzUncompressStream; | ||
|
||
function checkDependency() { | ||
try { | ||
require('lzma-native'); | ||
return true; | ||
} catch (err) { | ||
return false; | ||
} | ||
} | ||
|
||
function throwIfNoDependency() { | ||
if (!checkDependency()) { | ||
throw new Error('lzma-native is required for xz compression/decompression. Please install it with: npm install lzma-native'); | ||
} | ||
} | ||
|
||
// Lazy load the implementation | ||
function getImplementation() { | ||
if (!XzFileStream) { | ||
throwIfNoDependency(); | ||
XzFileStream = require('./file_stream'); | ||
XzUncompressStream = require('./uncompress_stream'); | ||
} | ||
return { XzFileStream, XzUncompressStream }; | ||
} | ||
|
||
exports.FileStream = function(opts) { | ||
const { XzFileStream } = getImplementation(); | ||
return new XzFileStream(opts); | ||
}; | ||
|
||
exports.UncompressStream = function(opts) { | ||
const { XzUncompressStream } = getImplementation(); | ||
return new XzUncompressStream(opts); | ||
}; | ||
|
||
exports.compressFile = function(source, dest, opts) { | ||
throwIfNoDependency(); | ||
const { XzFileStream } = getImplementation(); | ||
return utils.makeFileProcessFn(XzFileStream)(source, dest, opts); | ||
}; | ||
|
||
exports.uncompress = function(source, dest, opts) { | ||
throwIfNoDependency(); | ||
const { XzUncompressStream } = getImplementation(); | ||
return utils.makeFileProcessFn(XzUncompressStream)(source, dest, opts); | ||
}; | ||
|
||
exports.decompress = exports.uncompress; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
'use strict'; | ||
|
||
const fs = require('fs'); | ||
const lzma = require('lzma-native'); | ||
const utils = require('../utils'); | ||
const streamifier = require('streamifier'); | ||
const { PassThrough } = require('stream'); | ||
|
||
class XzUncompressStream extends PassThrough { | ||
constructor(opts) { | ||
opts = opts || {}; | ||
super(opts); | ||
|
||
const sourceType = utils.sourceType(opts.source); | ||
// Set text mode to true to handle line endings correctly on Windows | ||
const decompressor = lzma.createDecompressor({ | ||
...opts.lzma, | ||
textMode: true, | ||
}); | ||
|
||
decompressor.on('error', err => this.emit('error', err)); | ||
decompressor.on('end', () => this.end()); | ||
|
||
// Handle single file decompression | ||
if (sourceType === 'file') { | ||
const stream = fs.createReadStream(opts.source, opts.fs); | ||
stream.on('error', err => this.emit('error', err)); | ||
stream.pipe(decompressor).pipe(this); | ||
return; | ||
} | ||
|
||
if (sourceType === 'buffer') { | ||
const stream = streamifier.createReadStream(opts.source, opts.streamifier); | ||
stream.on('error', err => this.emit('error', err)); | ||
stream.pipe(decompressor).pipe(this); | ||
return; | ||
} | ||
|
||
if (sourceType === 'stream') { | ||
opts.source.on('error', err => this.emit('error', err)); | ||
opts.source.pipe(decompressor).pipe(this); | ||
return; | ||
} | ||
|
||
// For streaming input | ||
this.on('pipe', srcStream => { | ||
srcStream.unpipe(this); | ||
srcStream.pipe(decompressor).pipe(this); | ||
}); | ||
Comment on lines
+46
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to |
||
} | ||
} | ||
|
||
module.exports = XzUncompressStream; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const assert = require('assert'); | ||
const compressing = require('../..'); | ||
|
||
describe('test/xz/file_stream.test.js', () => { | ||
const sourceFile = path.join(__dirname, '../fixtures/xx.log'); | ||
const xzFile = path.join(__dirname, '../fixtures/xx.log.xz'); | ||
|
||
it('should compress file to xz', done => { | ||
const dest = path.join(__dirname, '../fixtures/xx.log.xz.tmp'); | ||
compressing.xz.compressFile(sourceFile, dest) | ||
.then(() => { | ||
assert(fs.existsSync(dest)); | ||
// 文件大小应该小于原始文件 | ||
assert(fs.statSync(dest).size < fs.statSync(sourceFile).size); | ||
fs.unlinkSync(dest); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
|
||
it('should decompress xz file to log', done => { | ||
const dest = path.join(__dirname, '../fixtures/xx.log.tmp'); | ||
compressing.xz.uncompress(xzFile, dest) | ||
.then(() => { | ||
assert(fs.existsSync(dest)); | ||
// 内容应该一致 | ||
const raw = fs.readFileSync(sourceFile); | ||
const out = fs.readFileSync(dest); | ||
assert.equal(out.length, raw.length); | ||
assert.deepEqual(out, raw); | ||
fs.unlinkSync(dest); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
|
||
it('should compress buffer to xz', done => { | ||
const buf = fs.readFileSync(sourceFile); | ||
const dest = path.join(__dirname, '../fixtures/xx.log.xz.tmp'); | ||
compressing.xz.compressFile(buf, dest) | ||
.then(() => { | ||
assert(fs.existsSync(dest)); | ||
fs.unlinkSync(dest); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
|
||
it('should decompress xz buffer to log', done => { | ||
const buf = fs.readFileSync(xzFile); | ||
const dest = path.join(__dirname, '../fixtures/xx.log.tmp'); | ||
compressing.xz.uncompress(buf, dest) | ||
.then(() => { | ||
assert(fs.existsSync(dest)); | ||
const raw = fs.readFileSync(sourceFile); | ||
const out = fs.readFileSync(dest); | ||
assert.equal(out.length, raw.length); | ||
assert.deepEqual(out, raw); | ||
fs.unlinkSync(dest); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
|
||
it('should compress/decompress utf-8 text to xz', async () => { | ||
const buf = Buffer.from('你好\nhello xz\nWindows\r\n'); | ||
const dest = path.join(__dirname, '../fixtures/xx.log.xz.utf8.tmp'); | ||
await compressing.xz.compressFile(buf, dest); | ||
assert(fs.existsSync(dest)); | ||
|
||
const dest2 = path.join(__dirname, '../fixtures/xx.log.utf8.tmp'); | ||
const xzBuf = fs.readFileSync(dest); | ||
await compressing.xz.uncompress(xzBuf, dest2); | ||
const outBuf = fs.readFileSync(dest2); | ||
assert.deepEqual(outBuf.toString(), buf.toString()); | ||
|
||
fs.unlinkSync(dest); | ||
fs.unlinkSync(dest2); | ||
}); | ||
|
||
it('should compress stream to xz', done => { | ||
const src = fs.createReadStream(sourceFile); | ||
const dest = path.join(__dirname, '../fixtures/xx.log.xz.tmp'); | ||
compressing.xz.compressFile(src, dest) | ||
.then(() => { | ||
assert(fs.existsSync(dest)); | ||
fs.unlinkSync(dest); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
|
||
it('should decompress xz stream to log', done => { | ||
const src = fs.createReadStream(xzFile); | ||
const dest = path.join(__dirname, '../fixtures/xx.log.tmp'); | ||
compressing.xz.uncompress(src, dest) | ||
.then(() => { | ||
assert(fs.existsSync(dest)); | ||
const raw = fs.readFileSync(sourceFile); | ||
const out = fs.readFileSync(dest); | ||
assert.equal(out.length, raw.length); | ||
assert.equal(out.toString(), raw.toString()); | ||
fs.unlinkSync(dest); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
xz.UncompressStream
is defined as extendingWriteStream
. However, its JavaScript implementation (lib/xz/uncompress_stream.js
) pipes the decompressed data out of itself (decompressor.pipe(this)
), making it behave as aReadable
stream. To ensure type correctness and consistency with its runtime behavior, this type definition should extendReadStream
.