Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e06367b
feat(io): add SeekableWriter interface and Uint8ArraySeekableWriter
KazW Jan 28, 2026
2d1d986
fix(io): address code review feedback for SeekableWriter
KazW Jan 28, 2026
6783f76
test: add prependZip() regression tests before refactoring
KazW Jan 28, 2026
70c827c
chore: add TODO for openExisting/prependZip refactoring
KazW Jan 28, 2026
d5b3a30
feat(zip-writer): add updateEntry() for metadata modification
KazW Jan 28, 2026
dc402a1
feat(zip-writer): add compact() to reclaim space from removed entries
KazW Jan 28, 2026
6582f05
feat(zip-writer): add dryRun option to compact()
KazW Jan 28, 2026
308620f
fix(zip-writer): properly calculate entry size with all extra field t…
KazW Jan 28, 2026
8f4d267
docs(types): add TypeScript definitions for incremental update API
KazW Jan 28, 2026
6ecd49a
test: add comprehensive edge case tests for incremental updates
KazW Jan 28, 2026
c3edc6e
docs: document incremental archive update API
KazW Jan 28, 2026
0d5304a
feat(io): add FileHandleWriter for Node.js file system support
KazW Jan 28, 2026
5ea2854
feat(io): add FileSystemAccessSeekableWriter for browser File System …
KazW Jan 28, 2026
596f73e
fix(io): improve FileSystemAccessSeekableWriter based on code review
KazW Jan 28, 2026
5dbf1cd
fix(zip-writer): preserve CRC32 for existing entries in openExisting/…
KazW Jan 28, 2026
56f9910
fix(zip-writer): read local header extra field length in openExisting
KazW Jan 28, 2026
3dec97a
fix(io): handle 64-bit file offsets in FileHandleWriter for >2GB files
KazW Jan 28, 2026
732ca06
revert(io): remove BigInt conversion that breaks fresh file writes
KazW Jan 28, 2026
93dc7dd
fix(zip-writer): support ZIP64 archives in openExisting without memor…
KazW Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 364 additions & 10 deletions docs/classes/ZipWriter.md

Large diffs are not rendered by default.

307 changes: 307 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,180 @@ export class Uint8ArrayWriter extends Writer<Uint8Array<ArrayBuffer>> {
constructor(defaultBufferSize?: number);
}

/**
* Represents an instance used to write data with random access (seeking) capabilities.
*
* SeekableWriter extends the basic Writer interface with seek, truncate, and readAt
* operations, enabling incremental updates to existing zip archives.
*/
export class SeekableWriter<Type> extends Writer<Type> {
/**
* The current write position in bytes.
*/
readonly position: number;
/**
* `true` to indicate this writer supports seeking operations.
*/
readonly isSeekable: true;
/**
* The total size of the written data in bytes.
*/
size: number;
/**
* Moves the write position to a specific offset.
*
* @param offset The byte offset to seek to.
* @returns A promise that resolves when the seek is complete.
*/
seek(offset: number): Promise<void>;
/**
* Truncates the data at the current position.
*
* @returns A promise that resolves when truncation is complete.
*/
truncate(): Promise<void>;
/**
* Reads data from a specific position without changing the current write position.
*
* @param offset The byte offset to read from.
* @param length The number of bytes to read.
* @returns A promise resolving to the data read.
*/
readAt(offset: number, length: number): Promise<Uint8Array>;
}

/**
* Represents a {@link SeekableWriter} instance that stores data in memory as a `Uint8Array`.
*
* This implementation automatically grows its internal buffer as needed and supports
* all seekable operations required for incremental zip archive updates.
*/
export class Uint8ArraySeekableWriter extends SeekableWriter<Uint8Array> {
/**
* Creates the {@link Uint8ArraySeekableWriter} instance
*
* @param initialSize The initial size of the internal buffer (default: 1024 bytes).
*/
constructor(initialSize?: number);
/**
* Initializes the writer.
*
* @returns A promise that resolves when initialization is complete.
*/
init(): Promise<void>;
/**
* Retrieves the written data as a `Uint8Array`.
*
* @returns The data written to the buffer, truncated to the actual size.
* Note: The implementation is synchronous but returns a Promise-compatible type
* to match the Writer base class signature.
*/
getData(): Promise<Uint8Array>;
}

/**
* Represents a {@link SeekableWriter} instance that writes data to a Node.js file handle.
*
* This implementation enables in-place ZIP modification on real files using Node.js
* fs.promises file handles. The file handle must be opened with read/write access.
*
* @example
* ```ts
* import { open } from 'node:fs/promises';
* import { FileHandleWriter, ZipWriter, TextReader } from '@zip.js/zip.js';
*
* const handle = await open('archive.zip', 'w+');
* const writer = new FileHandleWriter(handle);
* await writer.init();
*
* const zipWriter = new ZipWriter(writer);
* await zipWriter.add('file.txt', new TextReader('content'));
* await zipWriter.close();
* await handle.close();
* ```
*/
export class FileHandleWriter extends SeekableWriter<Uint8Array> {
/**
* Creates the {@link FileHandleWriter} instance
*
* @param fileHandle A Node.js file handle opened with fs.promises.open().
*/
constructor(fileHandle: FileHandle);
/**
* Initializes the writer by reading the current file size.
*
* @returns A promise that resolves when initialization is complete.
*/
init(): Promise<void>;
}

/**
* Represents a Node.js file handle from fs.promises.open().
* This is a minimal type definition for the FileHandleWriter constructor parameter.
*/
interface FileHandle {
stat(): Promise<{ size: number }>;
write(buffer: Uint8Array, offset: number, length: number, position: number): Promise<{ bytesWritten: number }>;
read(buffer: Uint8Array, offset: number, length: number, position: number): Promise<{ bytesRead: number }>;
truncate(length: number): Promise<void>;
}

/**
* Represents a {@link SeekableWriter} instance that writes data using the browser
* File System Access API (Chrome/Edge).
*
* This implementation enables in-place ZIP modification on real files in the browser
* using `showSaveFilePicker()` and `FileSystemWritableFileStream`.
*
* @example
* ```ts
* import { FileSystemAccessSeekableWriter, ZipWriter, TextReader } from '@zip.js/zip.js';
*
* // Get a file handle from the user
* const fileHandle = await window.showSaveFilePicker({
* suggestedName: 'archive.zip',
* types: [{ description: 'ZIP files', accept: { 'application/zip': ['.zip'] } }]
* });
*
* // Create writable stream
* const writableStream = await fileHandle.createWritable();
*
* // Create writer (pass fileHandle for readAt support)
* const writer = new FileSystemAccessSeekableWriter(writableStream, fileHandle);
* await writer.init();
*
* const zipWriter = new ZipWriter(writer);
* await zipWriter.add('file.txt', new TextReader('content'));
* await zipWriter.close();
* await writer.close();
* ```
*/
export class FileSystemAccessSeekableWriter extends SeekableWriter<Uint8Array> {
/**
* Creates the {@link FileSystemAccessSeekableWriter} instance
*
* @param writableFileStream A FileSystemWritableFileStream from createWritable().
* @param fileHandle Optional FileSystemFileHandle for readAt() support.
*/
constructor(writableFileStream: FileSystemWritableFileStream, fileHandle?: FileSystemFileHandle);
/**
* Initializes the writer.
*
* If `fileHandle` was provided in the constructor and `size` is omitted,
* the file size is automatically detected.
*
* @param size Optional initial size of the file. Auto-detected if fileHandle provided.
* @returns A promise that resolves when initialization is complete.
*/
init(size?: number): Promise<void>;
/**
* Closes the underlying writable stream.
*
* @returns A promise that resolves when the stream is closed.
*/
close(): Promise<void>;
}

/**
* Represents an instance used to create an unzipped stream.
*
Expand Down Expand Up @@ -1288,10 +1462,28 @@ export class ZipWriter<Type> {
>,
options?: ZipWriterConstructorOptions
);
/**
* Opens an existing zip archive for incremental updates.
*
* This static factory method reads an existing zip file and creates a {@link ZipWriter}
* positioned to append new entries while preserving all existing entries.
*
* @param writer A {@link SeekableWriter} instance containing the existing zip archive.
* @param options The options.
* @returns A promise resolving to a {@link ZipWriter} instance ready for incremental updates.
*/
static openExisting<WriterType>(
writer: SeekableWriter<WriterType>,
options?: ZipWriterConstructorOptions
): Promise<ZipWriter<WriterType>>;
/**
* `true` if the zip contains at least one entry that has been partially written.
*/
readonly hasCorruptedEntries?: boolean;
/**
* `true` if the underlying writer supports seeking operations (is a {@link SeekableWriter}).
*/
readonly isSeekable: boolean;

/**
* Adds an existing zip file at the beginning of the current zip. This method
Expand Down Expand Up @@ -1339,6 +1531,32 @@ export class ZipWriter<Type> {
*/
remove(entry: Entry | string): boolean;

/**
* Updates the metadata of an existing entry in the zip file.
*
* This method allows modifying certain metadata fields of an entry that has already been added
* to the archive. Note that the comment can only be updated if the new comment is equal to or
* shorter than the existing comment.
*
* @param entry The entry to update. This can be an {@link Entry} instance or the filename of the entry.
* @param metadata The metadata fields to update.
* @returns `true` if the entry was successfully updated, `false` if the entry was not found
* or if the comment update failed due to length constraints.
*/
updateEntry(entry: Entry | string, metadata: EntryMetadataUpdate): boolean;

/**
* Compacts the zip archive by removing gaps left by deleted entries.
*
* This method requires a {@link SeekableWriter} and will move entry data to eliminate
* fragmentation in the archive. Use the `dryRun` option to preview space savings without
* modifying the archive.
*
* @param options The options.
* @returns A promise resolving to the compact result with space savings information.
*/
compact(options?: CompactOptions): Promise<CompactResult>;

/**
* Writes the entries directory, writes the global comment, and returns the content of the zip file
*
Expand Down Expand Up @@ -1404,6 +1622,95 @@ export interface ZipWriterCloseOptions extends EntryOnprogressOptions {
preventClose?: boolean;
}

/**
* Represents the options passed to {@link ZipWriter#compact}.
*/
export interface CompactOptions {
/**
* The `AbortSignal` instance used to cancel the compact operation.
*/
signal?: AbortSignal;
/**
* The function called during the compact operation to report progress.
*
* @param progress The progress information.
*/
onProgress?(progress: CompactProgress): void;
/**
* `true` to calculate space savings without actually moving data.
*
* When set to `true`, the compact operation will analyze the archive and return
* how much space could be reclaimed without modifying the archive.
*
* @defaultValue false
*/
dryRun?: boolean;
}

/**
* Represents the progress information passed to {@link CompactOptions#onProgress}.
*/
export interface CompactProgress {
/**
* The number of entries processed so far.
*/
entriesProcessed: number;
/**
* The total number of entries to process.
*/
totalEntries: number;
/**
* The number of bytes reclaimed so far.
*/
reclaimedBytes: number;
}

/**
* Represents the result returned by {@link ZipWriter#compact}.
*/
export interface CompactResult {
/**
* The total number of bytes reclaimed by the compact operation.
*/
reclaimedBytes: number;
/**
* The number of entries that were moved to fill gaps.
*/
entriesMoved: number;
}

/**
* Represents the metadata fields that can be updated via {@link ZipWriter#updateEntry}.
*/
export interface EntryMetadataUpdate {
/**
* The last modification date.
*/
lastModDate?: Date;
/**
* The last access date.
*/
lastAccessDate?: Date;
/**
* The creation date.
*/
creationDate?: Date;
/**
* The external file attributes (raw).
*/
externalFileAttributes?: number;
/**
* The internal file attributes (raw).
*/
internalFileAttributes?: number;
/**
* The comment of the entry.
*
* Note: The new comment must be equal to or shorter than the existing comment.
*/
comment?: string;
}

/**
* Represents options passed to the constructor of {@link ZipWriter}, {@link ZipWriter#add} and `{@link ZipDirectoryEntry}#export*`.
*/
Expand Down
Loading