feat(archive): introduce ts_shared browser-compatible layer, refactor Node-specific tools to wrap/shared implementations, and modernize archive handling

This commit is contained in:
2026-01-01 23:09:06 +00:00
parent 4e3c5a8443
commit 6393527c95
37 changed files with 2850 additions and 5105 deletions

View File

@@ -6,12 +6,15 @@ import type {
TArchiveFormat,
TCompressionLevel,
TEntryFilter,
} from './interfaces.js';
} from '../ts_shared/interfaces.js';
import { Bzip2Tools } from './classes.bzip2tools.js';
import { GzipTools } from './classes.gziptools.js';
// Import browser-compatible tools from ts_shared
import { Bzip2Tools } from '../ts_shared/classes.bzip2tools.js';
import { GzipTools } from '../ts_shared/classes.gziptools.js';
import { ZipTools } from '../ts_shared/classes.ziptools.js';
// Import Node.js-extended TarTools
import { TarTools } from './classes.tartools.js';
import { ZipTools } from './classes.ziptools.js';
import { ArchiveAnalyzer, type IAnalyzedResult } from './classes.archiveanalyzer.js';
/**
@@ -62,7 +65,7 @@ export class SmartArchive {
public tarTools = new TarTools();
public zipTools = new ZipTools();
public gzipTools = new GzipTools();
public bzip2Tools = new Bzip2Tools(this);
public bzip2Tools = new Bzip2Tools();
public archiveAnalyzer = new ArchiveAnalyzer(this);
// ============================================
@@ -173,7 +176,7 @@ export class SmartArchive {
public entry(archivePath: string, content: string | Buffer): this {
this.ensureNotInExtractMode('entry');
if (!this._mode) this._mode = 'create';
this.pendingEntries.push({ archivePath, content });
this.pendingEntries.push({ archivePath, content: content instanceof Buffer ? new Uint8Array(content) : content });
return this;
}
@@ -184,7 +187,10 @@ export class SmartArchive {
this.ensureNotInExtractMode('entries');
if (!this._mode) this._mode = 'create';
for (const e of entriesArg) {
this.pendingEntries.push({ archivePath: e.archivePath, content: e.content });
this.pendingEntries.push({
archivePath: e.archivePath,
content: e.content instanceof Buffer ? new Uint8Array(e.content) : e.content
});
}
return this;
}
@@ -374,30 +380,36 @@ export class SmartArchive {
plugins.smartstream.createTransformFunction<IAnalyzedResult, void>(
async (analyzedResultChunk) => {
if (analyzedResultChunk.fileType?.mime === 'application/x-tar') {
const tarStream = analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
// Use modern-tar for TAR extraction
const chunks: Buffer[] = [];
tarStream.on('entry', async (header, stream, next) => {
if (header.type === 'directory') {
stream.resume();
stream.on('end', () => next());
return;
analyzedResultChunk.resultStream.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
analyzedResultChunk.resultStream.on('end', async () => {
try {
const tarBuffer = Buffer.concat(chunks);
const entries = await this.tarTools.extractTar(new Uint8Array(tarBuffer));
for (const entry of entries) {
if (entry.isDirectory) continue;
const streamFile = plugins.smartfile.StreamFile.fromBuffer(
Buffer.from(entry.content)
);
streamFile.relativeFilePath = entry.path;
streamFileIntake.push(streamFile);
}
safeSignalEnd();
} catch (err) {
streamFileIntake.emit('error', err);
}
const passThrough = new plugins.stream.PassThrough();
const streamfile = plugins.smartfile.StreamFile.fromStream(passThrough, header.name);
streamFileIntake.push(streamfile);
stream.pipe(passThrough);
stream.on('end', () => {
passThrough.end();
next();
});
});
tarStream.on('finish', () => {
safeSignalEnd();
analyzedResultChunk.resultStream.on('error', (err: Error) => {
streamFileIntake.emit('error', err);
});
analyzedResultChunk.resultStream.pipe(analyzedResultChunk.decompressionStream);
} else if (analyzedResultChunk.fileType?.mime === 'application/zip') {
analyzedResultChunk.resultStream
.pipe(analyzedResultChunk.decompressionStream)
@@ -544,25 +556,29 @@ export class SmartArchive {
if (this.creationFormat === 'tar' || this.creationFormat === 'tar.gz' || this.creationFormat === 'tgz') {
if (this.creationFormat === 'tar') {
this.archiveBuffer = await this.tarTools.packFiles(entries);
const result = await this.tarTools.packFiles(entries);
this.archiveBuffer = Buffer.from(result);
} else {
this.archiveBuffer = await this.tarTools.packFilesToTarGz(entries, this._compressionLevel);
const result = await this.tarTools.packFilesToTarGz(entries, this._compressionLevel);
this.archiveBuffer = Buffer.from(result);
}
} else if (this.creationFormat === 'zip') {
this.archiveBuffer = await this.zipTools.createZip(entries, this._compressionLevel);
const result = await this.zipTools.createZip(entries, this._compressionLevel);
this.archiveBuffer = Buffer.from(result);
} else if (this.creationFormat === 'gz') {
if (entries.length !== 1) {
throw new Error('GZIP format only supports a single file');
}
let content: Buffer;
let content: Uint8Array;
if (typeof entries[0].content === 'string') {
content = Buffer.from(entries[0].content);
} else if (Buffer.isBuffer(entries[0].content)) {
content = new TextEncoder().encode(entries[0].content);
} else if (entries[0].content instanceof Uint8Array) {
content = entries[0].content;
} else {
throw new Error('GZIP format requires string or Buffer content');
throw new Error('GZIP format requires string or Uint8Array content');
}
this.archiveBuffer = await this.gzipTools.compress(content, this._compressionLevel);
const result = await this.gzipTools.compress(content, this._compressionLevel);
this.archiveBuffer = Buffer.from(result);
} else {
throw new Error(`Unsupported format: ${this.creationFormat}`);
}
@@ -808,7 +824,7 @@ export class SmartArchive {
const content = await plugins.fsPromises.readFile(absolutePath);
this.pendingEntries.push({
archivePath,
content,
content: new Uint8Array(content),
});
}
}