BREAKING CHANGE(SmartArchive): Refactor public API: rename factory/extraction methods, introduce typed interfaces and improved compression tools
This commit is contained in:
@@ -1,75 +1,267 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import type {
|
||||
IArchiveCreationOptions,
|
||||
IArchiveEntry,
|
||||
IArchiveExtractionOptions,
|
||||
IArchiveEntryInfo,
|
||||
IArchiveInfo,
|
||||
TArchiveFormat,
|
||||
TCompressionLevel,
|
||||
} from './interfaces.js';
|
||||
|
||||
import { Bzip2Tools } from './classes.bzip2tools.js';
|
||||
import { GzipTools } from './classes.gziptools.js';
|
||||
import { TarTools } from './classes.tartools.js';
|
||||
import { ZipTools } from './classes.ziptools.js';
|
||||
import { ArchiveAnalyzer, type IAnalyzedResult } from './classes.archiveanalyzer.js';
|
||||
|
||||
import {
|
||||
ArchiveAnalyzer,
|
||||
type IAnalyzedResult,
|
||||
} from './classes.archiveanalyzer.js';
|
||||
|
||||
import type { from } from '@push.rocks/smartrx/dist_ts/smartrx.plugins.rxjs.js';
|
||||
|
||||
/**
|
||||
* Main class for archive manipulation
|
||||
* Supports TAR, ZIP, GZIP, and BZIP2 formats
|
||||
*/
|
||||
export class SmartArchive {
|
||||
// STATIC
|
||||
public static async fromArchiveUrl(urlArg: string): Promise<SmartArchive> {
|
||||
// ============================================
|
||||
// STATIC FACTORY METHODS - EXTRACTION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create SmartArchive from a URL
|
||||
*/
|
||||
public static async fromUrl(urlArg: string): Promise<SmartArchive> {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.sourceUrl = urlArg;
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
public static async fromArchiveFile(
|
||||
filePathArg: string,
|
||||
): Promise<SmartArchive> {
|
||||
/**
|
||||
* Create SmartArchive from a local file path
|
||||
*/
|
||||
public static async fromFile(filePathArg: string): Promise<SmartArchive> {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.sourceFilePath = filePathArg;
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
public static async fromArchiveStream(
|
||||
streamArg:
|
||||
| plugins.stream.Readable
|
||||
| plugins.stream.Duplex
|
||||
| plugins.stream.Transform,
|
||||
/**
|
||||
* Create SmartArchive from a readable stream
|
||||
*/
|
||||
public static async fromStream(
|
||||
streamArg: plugins.stream.Readable | plugins.stream.Duplex | plugins.stream.Transform
|
||||
): Promise<SmartArchive> {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.sourceStream = streamArg;
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
/**
|
||||
* Create SmartArchive from an in-memory buffer
|
||||
*/
|
||||
public static async fromBuffer(buffer: Buffer): Promise<SmartArchive> {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.sourceStream = plugins.stream.Readable.from(buffer);
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// STATIC FACTORY METHODS - CREATION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create a new archive from a directory
|
||||
*/
|
||||
public static async fromDirectory(
|
||||
directoryPath: string,
|
||||
options: IArchiveCreationOptions
|
||||
): Promise<SmartArchive> {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.creationOptions = options;
|
||||
|
||||
const tarTools = new TarTools();
|
||||
|
||||
if (options.format === 'tar' || options.format === 'tar.gz' || options.format === 'tgz') {
|
||||
if (options.format === 'tar') {
|
||||
const pack = await tarTools.packDirectory(directoryPath);
|
||||
pack.finalize();
|
||||
smartArchiveInstance.archiveBuffer = await SmartArchive.streamToBuffer(pack);
|
||||
} else {
|
||||
smartArchiveInstance.archiveBuffer = await tarTools.packDirectoryToTarGz(
|
||||
directoryPath,
|
||||
options.compressionLevel
|
||||
);
|
||||
}
|
||||
} else if (options.format === 'zip') {
|
||||
const zipTools = new ZipTools();
|
||||
const fileTree = await plugins.listFileTree(directoryPath, '**/*');
|
||||
const entries: IArchiveEntry[] = [];
|
||||
|
||||
for (const filePath of fileTree) {
|
||||
const absolutePath = plugins.path.join(directoryPath, filePath);
|
||||
const content = await plugins.fsPromises.readFile(absolutePath);
|
||||
entries.push({
|
||||
archivePath: filePath,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
smartArchiveInstance.archiveBuffer = await zipTools.createZip(entries, options.compressionLevel);
|
||||
} else {
|
||||
throw new Error(`Unsupported format for directory packing: ${options.format}`);
|
||||
}
|
||||
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new archive from an array of entries
|
||||
*/
|
||||
public static async fromFiles(
|
||||
files: IArchiveEntry[],
|
||||
options: IArchiveCreationOptions
|
||||
): Promise<SmartArchive> {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.creationOptions = options;
|
||||
|
||||
if (options.format === 'tar' || options.format === 'tar.gz' || options.format === 'tgz') {
|
||||
const tarTools = new TarTools();
|
||||
if (options.format === 'tar') {
|
||||
smartArchiveInstance.archiveBuffer = await tarTools.packFiles(files);
|
||||
} else {
|
||||
smartArchiveInstance.archiveBuffer = await tarTools.packFilesToTarGz(files, options.compressionLevel);
|
||||
}
|
||||
} else if (options.format === 'zip') {
|
||||
const zipTools = new ZipTools();
|
||||
smartArchiveInstance.archiveBuffer = await zipTools.createZip(files, options.compressionLevel);
|
||||
} else if (options.format === 'gz') {
|
||||
if (files.length !== 1) {
|
||||
throw new Error('GZIP format only supports a single file');
|
||||
}
|
||||
const gzipTools = new GzipTools();
|
||||
let content: Buffer;
|
||||
if (typeof files[0].content === 'string') {
|
||||
content = Buffer.from(files[0].content);
|
||||
} else if (Buffer.isBuffer(files[0].content)) {
|
||||
content = files[0].content;
|
||||
} else {
|
||||
throw new Error('GZIP format requires string or Buffer content');
|
||||
}
|
||||
smartArchiveInstance.archiveBuffer = await gzipTools.compress(content, options.compressionLevel);
|
||||
} else {
|
||||
throw new Error(`Unsupported format: ${options.format}`);
|
||||
}
|
||||
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building an archive incrementally using a builder pattern
|
||||
*/
|
||||
public static create(options: IArchiveCreationOptions): SmartArchive {
|
||||
const smartArchiveInstance = new SmartArchive();
|
||||
smartArchiveInstance.creationOptions = options;
|
||||
smartArchiveInstance.pendingEntries = [];
|
||||
return smartArchiveInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to convert a stream to buffer
|
||||
*/
|
||||
private static async streamToBuffer(stream: plugins.stream.Readable): Promise<Buffer> {
|
||||
const chunks: Buffer[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INSTANCE PROPERTIES
|
||||
// ============================================
|
||||
|
||||
public tarTools = new TarTools();
|
||||
public zipTools = new ZipTools();
|
||||
public gzipTools = new GzipTools();
|
||||
public bzip2Tools = new Bzip2Tools(this);
|
||||
public archiveAnalyzer = new ArchiveAnalyzer(this);
|
||||
|
||||
public sourceUrl: string;
|
||||
public sourceFilePath: string;
|
||||
public sourceStream:
|
||||
| plugins.stream.Readable
|
||||
| plugins.stream.Duplex
|
||||
| plugins.stream.Transform;
|
||||
public sourceUrl?: string;
|
||||
public sourceFilePath?: string;
|
||||
public sourceStream?: plugins.stream.Readable | plugins.stream.Duplex | plugins.stream.Transform;
|
||||
|
||||
public archiveName: string;
|
||||
public singleFileMode: boolean = false;
|
||||
|
||||
public addedDirectories: string[] = [];
|
||||
public addedFiles: (
|
||||
| plugins.smartfile.SmartFile
|
||||
| plugins.smartfile.StreamFile
|
||||
)[] = [];
|
||||
public addedUrls: string[] = [];
|
||||
private archiveBuffer?: Buffer;
|
||||
private creationOptions?: IArchiveCreationOptions;
|
||||
private pendingEntries?: IArchiveEntry[];
|
||||
|
||||
constructor() {}
|
||||
|
||||
// ============================================
|
||||
// BUILDER METHODS (for incremental creation)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* gets the original archive stream
|
||||
* Add a file to the archive (builder pattern)
|
||||
*/
|
||||
public async getArchiveStream() {
|
||||
public addFile(archivePath: string, content: string | Buffer): this {
|
||||
if (!this.pendingEntries) {
|
||||
throw new Error('addFile can only be called on archives created with SmartArchive.create()');
|
||||
}
|
||||
this.pendingEntries.push({ archivePath, content });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a SmartFile to the archive (builder pattern)
|
||||
*/
|
||||
public addSmartFile(file: plugins.smartfile.SmartFile, archivePath?: string): this {
|
||||
if (!this.pendingEntries) {
|
||||
throw new Error('addSmartFile can only be called on archives created with SmartArchive.create()');
|
||||
}
|
||||
this.pendingEntries.push({
|
||||
archivePath: archivePath || file.relative,
|
||||
content: file,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a StreamFile to the archive (builder pattern)
|
||||
*/
|
||||
public addStreamFile(file: plugins.smartfile.StreamFile, archivePath?: string): this {
|
||||
if (!this.pendingEntries) {
|
||||
throw new Error('addStreamFile can only be called on archives created with SmartArchive.create()');
|
||||
}
|
||||
this.pendingEntries.push({
|
||||
archivePath: archivePath || file.relativeFilePath,
|
||||
content: file,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the archive from pending entries
|
||||
*/
|
||||
public async build(): Promise<SmartArchive> {
|
||||
if (!this.pendingEntries || !this.creationOptions) {
|
||||
throw new Error('build can only be called on archives created with SmartArchive.create()');
|
||||
}
|
||||
|
||||
const built = await SmartArchive.fromFiles(this.pendingEntries, this.creationOptions);
|
||||
this.archiveBuffer = built.archiveBuffer;
|
||||
this.pendingEntries = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXTRACTION METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get the original archive stream
|
||||
*/
|
||||
public async toStream(): Promise<plugins.stream.Readable> {
|
||||
if (this.archiveBuffer) {
|
||||
return plugins.stream.Readable.from(this.archiveBuffer);
|
||||
}
|
||||
if (this.sourceStream) {
|
||||
return this.sourceStream;
|
||||
}
|
||||
@@ -78,161 +270,320 @@ export class SmartArchive {
|
||||
.url(this.sourceUrl)
|
||||
.get();
|
||||
const webStream = response.stream();
|
||||
// @ts-ignore - Web stream to Node.js stream conversion
|
||||
const urlStream = plugins.stream.Readable.fromWeb(webStream);
|
||||
return urlStream;
|
||||
return plugins.stream.Readable.fromWeb(webStream as any);
|
||||
}
|
||||
if (this.sourceFilePath) {
|
||||
const fileStream = plugins.fs.createReadStream(this.sourceFilePath);
|
||||
return fileStream;
|
||||
return plugins.fs.createReadStream(this.sourceFilePath);
|
||||
}
|
||||
throw new Error('No archive source configured');
|
||||
}
|
||||
|
||||
public async exportToTarGzStream() {
|
||||
const tarPackStream = await this.tarTools.getPackStream();
|
||||
const gzipStream = await this.gzipTools.getCompressionStream();
|
||||
// const archiveStream = tarPackStream.pipe(gzipStream);
|
||||
// return archiveStream;
|
||||
/**
|
||||
* Get archive as a Buffer
|
||||
*/
|
||||
public async toBuffer(): Promise<Buffer> {
|
||||
if (this.archiveBuffer) {
|
||||
return this.archiveBuffer;
|
||||
}
|
||||
const stream = await this.toStream();
|
||||
return SmartArchive.streamToBuffer(stream);
|
||||
}
|
||||
|
||||
public async exportToFs(
|
||||
/**
|
||||
* Write archive to a file
|
||||
*/
|
||||
public async toFile(filePath: string): Promise<void> {
|
||||
const buffer = await this.toBuffer();
|
||||
await plugins.fsPromises.mkdir(plugins.path.dirname(filePath), { recursive: true });
|
||||
await plugins.fsPromises.writeFile(filePath, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract archive to filesystem
|
||||
*/
|
||||
public async extractToDirectory(
|
||||
targetDir: string,
|
||||
fileNameArg?: string,
|
||||
options?: Partial<IArchiveExtractionOptions>
|
||||
): Promise<void> {
|
||||
const done = plugins.smartpromise.defer<void>();
|
||||
const streamFileStream = await this.exportToStreamOfStreamFiles();
|
||||
const streamFileStream = await this.extractToStream();
|
||||
|
||||
streamFileStream.pipe(
|
||||
new plugins.smartstream.SmartDuplex({
|
||||
objectMode: true,
|
||||
writeFunction: async (
|
||||
streamFileArg: plugins.smartfile.StreamFile,
|
||||
streamtools,
|
||||
) => {
|
||||
const done = plugins.smartpromise.defer<void>();
|
||||
console.log(
|
||||
streamFileArg.relativeFilePath
|
||||
? streamFileArg.relativeFilePath
|
||||
: 'no relative path',
|
||||
);
|
||||
writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
|
||||
const innerDone = plugins.smartpromise.defer<void>();
|
||||
const streamFile = streamFileArg;
|
||||
let relativePath = streamFile.relativeFilePath || options?.fileName || 'extracted_file';
|
||||
|
||||
// Apply stripComponents if specified
|
||||
if (options?.stripComponents && options.stripComponents > 0) {
|
||||
const parts = relativePath.split('/');
|
||||
relativePath = parts.slice(options.stripComponents).join('/');
|
||||
if (!relativePath) {
|
||||
innerDone.resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filter if specified
|
||||
if (options?.filter) {
|
||||
const entryInfo: IArchiveEntryInfo = {
|
||||
path: relativePath,
|
||||
size: 0,
|
||||
isDirectory: false,
|
||||
isFile: true,
|
||||
};
|
||||
if (!options.filter(entryInfo)) {
|
||||
innerDone.resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const readStream = await streamFile.createReadStream();
|
||||
await plugins.fsPromises.mkdir(targetDir, { recursive: true });
|
||||
const writePath = plugins.path.join(
|
||||
targetDir,
|
||||
streamFile.relativeFilePath || fileNameArg,
|
||||
);
|
||||
const writePath = plugins.path.join(targetDir, relativePath);
|
||||
await plugins.fsPromises.mkdir(plugins.path.dirname(writePath), { recursive: true });
|
||||
const writeStream = plugins.fs.createWriteStream(writePath);
|
||||
readStream.pipe(writeStream);
|
||||
writeStream.on('finish', () => {
|
||||
done.resolve();
|
||||
innerDone.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
await innerDone.promise;
|
||||
},
|
||||
finalFunction: async () => {
|
||||
done.resolve();
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
return done.promise;
|
||||
}
|
||||
|
||||
public async exportToStreamOfStreamFiles() {
|
||||
const streamFileIntake =
|
||||
new plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>({
|
||||
objectMode: true,
|
||||
});
|
||||
const archiveStream = await this.getArchiveStream();
|
||||
/**
|
||||
* Extract archive to a stream of StreamFile objects
|
||||
*/
|
||||
public async extractToStream(): Promise<plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>> {
|
||||
const streamFileIntake = new plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>({
|
||||
objectMode: true,
|
||||
});
|
||||
|
||||
// Guard to prevent multiple signalEnd calls
|
||||
let hasSignaledEnd = false;
|
||||
const safeSignalEnd = () => {
|
||||
if (!hasSignaledEnd) {
|
||||
hasSignaledEnd = true;
|
||||
streamFileIntake.signalEnd();
|
||||
}
|
||||
};
|
||||
|
||||
const archiveStream = await this.toStream();
|
||||
const createAnalyzedStream = () => this.archiveAnalyzer.getAnalyzedStream();
|
||||
|
||||
// lets create a function that can be called multiple times to unpack layers of archives
|
||||
const createUnpackStream = () =>
|
||||
plugins.smartstream.createTransformFunction<IAnalyzedResult, any>(
|
||||
plugins.smartstream.createTransformFunction<IAnalyzedResult, void>(
|
||||
async (analyzedResultChunk) => {
|
||||
if (analyzedResultChunk.fileType?.mime === 'application/x-tar') {
|
||||
const tarStream =
|
||||
analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
|
||||
const tarStream = analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
|
||||
|
||||
tarStream.on('entry', async (header, stream, next) => {
|
||||
if (header.type === 'directory') {
|
||||
console.log(
|
||||
`tar stream directory: ${header.name} ... skipping!`,
|
||||
);
|
||||
stream.resume(); // Consume directory stream
|
||||
stream.resume();
|
||||
stream.on('end', () => next());
|
||||
return;
|
||||
}
|
||||
console.log(`tar stream file: ${header.name}`);
|
||||
|
||||
// Create a PassThrough stream to buffer the data
|
||||
|
||||
const passThrough = new plugins.stream.PassThrough();
|
||||
const streamfile = plugins.smartfile.StreamFile.fromStream(
|
||||
passThrough,
|
||||
header.name,
|
||||
);
|
||||
|
||||
// Push the streamfile immediately
|
||||
const streamfile = plugins.smartfile.StreamFile.fromStream(passThrough, header.name);
|
||||
streamFileIntake.push(streamfile);
|
||||
|
||||
// Pipe the tar entry stream to the passthrough
|
||||
stream.pipe(passThrough);
|
||||
|
||||
// Move to next entry when this one ends
|
||||
stream.on('end', () => {
|
||||
passThrough.end();
|
||||
next();
|
||||
});
|
||||
});
|
||||
tarStream.on('finish', function () {
|
||||
console.log('tar extraction finished');
|
||||
// Only signal end if this is the final stream
|
||||
streamFileIntake.signalEnd();
|
||||
|
||||
tarStream.on('finish', () => {
|
||||
safeSignalEnd();
|
||||
});
|
||||
analyzedResultChunk.resultStream.pipe(
|
||||
analyzedResultChunk.decompressionStream,
|
||||
);
|
||||
|
||||
analyzedResultChunk.resultStream.pipe(analyzedResultChunk.decompressionStream);
|
||||
} else if (analyzedResultChunk.fileType?.mime === 'application/zip') {
|
||||
analyzedResultChunk.resultStream
|
||||
.pipe(analyzedResultChunk.decompressionStream)
|
||||
.pipe(
|
||||
new plugins.smartstream.SmartDuplex({
|
||||
objectMode: true,
|
||||
writeFunction: async (
|
||||
streamFileArg: plugins.smartfile.StreamFile,
|
||||
streamtools,
|
||||
) => {
|
||||
writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
|
||||
streamFileIntake.push(streamFileArg);
|
||||
},
|
||||
finalFunction: async () => {
|
||||
streamFileIntake.signalEnd();
|
||||
safeSignalEnd();
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
analyzedResultChunk.isArchive &&
|
||||
analyzedResultChunk.decompressionStream
|
||||
) {
|
||||
} else if (analyzedResultChunk.isArchive && analyzedResultChunk.decompressionStream) {
|
||||
// For nested archives (like gzip containing tar)
|
||||
const nestedStream = analyzedResultChunk.resultStream
|
||||
analyzedResultChunk.resultStream
|
||||
.pipe(analyzedResultChunk.decompressionStream)
|
||||
.pipe(createAnalyzedStream())
|
||||
.pipe(createUnpackStream());
|
||||
|
||||
// Don't signal end here - let the nested unpacker handle it
|
||||
} else {
|
||||
const streamFile = plugins.smartfile.StreamFile.fromStream(
|
||||
analyzedResultChunk.resultStream,
|
||||
analyzedResultChunk.fileType?.ext,
|
||||
analyzedResultChunk.fileType?.ext
|
||||
);
|
||||
streamFileIntake.push(streamFile);
|
||||
streamFileIntake.signalEnd();
|
||||
safeSignalEnd();
|
||||
}
|
||||
},
|
||||
{
|
||||
objectMode: true,
|
||||
},
|
||||
{ objectMode: true }
|
||||
);
|
||||
|
||||
archiveStream.pipe(createAnalyzedStream()).pipe(createUnpackStream());
|
||||
return streamFileIntake;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract archive to an array of SmartFile objects (in-memory)
|
||||
*/
|
||||
public async extractToSmartFiles(): Promise<plugins.smartfile.SmartFile[]> {
|
||||
const streamFiles = await this.extractToStream();
|
||||
const smartFiles: plugins.smartfile.SmartFile[] = [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
streamFiles.on('data', async (streamFile: plugins.smartfile.StreamFile) => {
|
||||
try {
|
||||
const smartFile = await streamFile.toSmartFile();
|
||||
smartFiles.push(smartFile);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
streamFiles.on('end', () => resolve(smartFiles));
|
||||
streamFiles.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a single file from the archive by path
|
||||
*/
|
||||
public async extractFile(filePath: string): Promise<plugins.smartfile.SmartFile | null> {
|
||||
const streamFiles = await this.extractToStream();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let found = false;
|
||||
|
||||
streamFiles.on('data', async (streamFile: plugins.smartfile.StreamFile) => {
|
||||
if (streamFile.relativeFilePath === filePath || streamFile.relativeFilePath?.endsWith(filePath)) {
|
||||
found = true;
|
||||
try {
|
||||
const smartFile = await streamFile.toSmartFile();
|
||||
resolve(smartFile);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
streamFiles.on('end', () => {
|
||||
if (!found) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
streamFiles.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ANALYSIS METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Analyze the archive and return metadata
|
||||
*/
|
||||
public async analyze(): Promise<IArchiveInfo> {
|
||||
const stream = await this.toStream();
|
||||
const firstChunk = await this.readFirstChunk(stream);
|
||||
const fileType = await plugins.fileType.fileTypeFromBuffer(firstChunk);
|
||||
|
||||
let format: TArchiveFormat | null = null;
|
||||
let isCompressed = false;
|
||||
let isArchive = false;
|
||||
|
||||
if (fileType) {
|
||||
switch (fileType.mime) {
|
||||
case 'application/gzip':
|
||||
format = 'gz';
|
||||
isCompressed = true;
|
||||
isArchive = true;
|
||||
break;
|
||||
case 'application/zip':
|
||||
format = 'zip';
|
||||
isCompressed = true;
|
||||
isArchive = true;
|
||||
break;
|
||||
case 'application/x-tar':
|
||||
format = 'tar';
|
||||
isArchive = true;
|
||||
break;
|
||||
case 'application/x-bzip2':
|
||||
format = 'bz2';
|
||||
isCompressed = true;
|
||||
isArchive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
format,
|
||||
isCompressed,
|
||||
isArchive,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List all entries in the archive without extracting
|
||||
*/
|
||||
public async listEntries(): Promise<IArchiveEntryInfo[]> {
|
||||
const entries: IArchiveEntryInfo[] = [];
|
||||
const streamFiles = await this.extractToStream();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
streamFiles.on('data', (streamFile: plugins.smartfile.StreamFile) => {
|
||||
entries.push({
|
||||
path: streamFile.relativeFilePath || 'unknown',
|
||||
size: 0, // Size not available without reading
|
||||
isDirectory: false,
|
||||
isFile: true,
|
||||
});
|
||||
});
|
||||
streamFiles.on('end', () => resolve(entries));
|
||||
streamFiles.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific file exists in the archive
|
||||
*/
|
||||
public async hasFile(filePath: string): Promise<boolean> {
|
||||
const entries = await this.listEntries();
|
||||
return entries.some((e) => e.path === filePath || e.path.endsWith(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to read first chunk from stream
|
||||
*/
|
||||
private async readFirstChunk(stream: plugins.stream.Readable): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onData = (chunk: Buffer) => {
|
||||
stream.removeListener('data', onData);
|
||||
stream.removeListener('error', reject);
|
||||
resolve(chunk);
|
||||
};
|
||||
stream.on('data', onData);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user