Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ If you need a feature, you're very welcome to [open an issue](https://github.com

Should be straightforward to implement if needed. Maybe `client-zip` should allow extending by third-party code so those extra fields can be plug-ins instead of built into the library.

The UNIX permissions in external attributes (ignored by many readers, though) are hardcoded to 664, could be made configurable.
<del>The UNIX permissions in external attributes (ignored by many readers, though) are hardcoded to 664, could be made configurable.</del> The UNIX permissions are now configurable via the `mode` field, set by default to 664 for files, 775 for folders.

### <del>ZIP64</del>

Expand Down
10 changes: 5 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ type StreamLike = ReadableStream<Uint8Array> | AsyncIterable<BufferLike>

/** The file name, modification date and size will be read from the input;
* extra arguments can be given to override the input’s metadata. */
type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint }
type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint, mode?: number }

/** Intrinsic size, but the file name must be provided and modification date can’t be guessed. */
type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint }
type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint, mode?: number }

/** The file name must be provided ; modification date and content length can’t be guessed. */
type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint }
type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint, mode?: number }

/** The folder name must be provided ; modification date can’t be guessed. */
type InputFolder = { name: any, lastModified?: any, input?: never, size?: never }
type InputFolder = { name: any, lastModified?: any, input?: never, size?: never, mode?: number }

/** Both filename and size must be provided ; input is not helpful here. */
type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint }
type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint, mode?: number }

type ForAwaitable<T> = AsyncIterable<T> | Iterable<T>

Expand Down
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { loadFiles, contentLength, ForAwaitable } from "./zip.ts"

/** The file name, modification date and size will be read from the input;
* extra arguments can be given to override the input’s metadata. */
type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint }
type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint, mode?: number }

/** Intrinsic size, but the file name must be provided and modification date can’t be guessed. */
type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint }
type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint, mode?: number }

/** The file name must be provided ; modification date and content length can’t be guessed. */
type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint }
type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint, mode?: number }

/** The folder name must be provided ; modification date can’t be guessed. */
type InputFolder = { name: any, lastModified?: any, input?: never, size?: never }
type InputFolder = { name: any, lastModified?: any, input?: never, size?: never, mode?: number }

/** Both filename and size must be provided ; input is not helpful here. */
type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint }
type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint, mode?: number }

export type Options = {
/** If provided, the returned Response will have its `Content-Length` header set to this value.
Expand All @@ -37,7 +37,7 @@ export type Options = {
function normalizeArgs(file: InputWithMeta | InputWithSizeMeta | InputWithoutMeta | InputFolder | JustMeta) {
return file instanceof File || file instanceof Response
? [[file], [file]] as const
: [[file.input, file.name, file.size], [file.input, file.lastModified]] as const
: [[file.input, file.name, file.size], [file.input, file.lastModified, file.mode]] as const
}

function* mapMeta(files: Iterable<InputWithMeta | InputWithSizeMeta | JustMeta | InputFolder>) {
Expand Down
36 changes: 23 additions & 13 deletions src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export type ZipFileDescription = {
modDate: Date
bytes: ReadableStream<Uint8Array> | Uint8Array | Promise<Uint8Array>
crc?: number // will be computed later
mode: number // UNIX permissions, 0o664 by default
isFile: true
}
export type ZipFolderDescription = {
modDate: Date
mode: number // UNIX permissions, 0o775 by default
isFile: false
}
export type ZipEntryDescription = ZipFileDescription | ZipFolderDescription;
Expand All @@ -19,30 +21,38 @@ export type ZipEntryDescription = ZipFileDescription | ZipFolderDescription;
* For other types of input, the `name` is required and `modDate` will default to *now*.
* @param modDate should be a Date or timestamp or anything else that works in `new Date()`
*/
export function normalizeInput(input: File | Response | BufferLike | StreamLike, modDate?: any): ZipFileDescription;
export function normalizeInput(input: undefined, modDate?: any): ZipFolderDescription;
export function normalizeInput(input?: File | Response | BufferLike | StreamLike, modDate?: any): ZipEntryDescription {
export function normalizeInput(input: File | Response | BufferLike | StreamLike, modDate?: any, mode?: number): ZipFileDescription;
export function normalizeInput(input: undefined, modDate?: any, mode?: number): ZipFolderDescription;
export function normalizeInput(input?: File | Response | BufferLike | StreamLike, modDate?: any, mode?: number): ZipEntryDescription {
if (modDate !== undefined && !(modDate instanceof Date)) modDate = new Date(modDate)

const isFile = input !== undefined

if(!mode) {
mode = isFile ? 0o664 : 0o775
}

if (input instanceof File) return {
isFile: true,
isFile,
modDate: modDate || new Date(input.lastModified),
bytes: input.stream()
bytes: input.stream(),
mode
}
if (input instanceof Response) return {
isFile: true,
isFile,
modDate: modDate || new Date(input.headers.get("Last-Modified") || Date.now()),
bytes: input.body!
bytes: input.body!,
mode
}

if (modDate === undefined) modDate = new Date()
else if (isNaN(modDate)) throw new Error("Invalid modification date.")
if (input === undefined) return { isFile: false, modDate }
if (typeof input === "string") return { isFile: true, modDate, bytes: encodeString(input) }
if (input instanceof Blob) return { isFile: true, modDate, bytes: input.stream() }
if (input instanceof Uint8Array || input instanceof ReadableStream) return { isFile: true, modDate, bytes: input }
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) return { isFile: true, modDate, bytes: makeUint8Array(input) }
if (Symbol.asyncIterator in input) return { isFile: true, modDate, bytes: ReadableFromIterator(input[Symbol.asyncIterator]()) }
if (!isFile) return { isFile, modDate, mode }
if (typeof input === "string") return { isFile, modDate, bytes: encodeString(input), mode }
if (input instanceof Blob) return { isFile, modDate, bytes: input.stream(), mode }
if (input instanceof Uint8Array || input instanceof ReadableStream) return { isFile, modDate, bytes: input, mode }
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) return { isFile, modDate, bytes: makeUint8Array(input), mode }
if (Symbol.asyncIterator in input) return { isFile, modDate, bytes: ReadableFromIterator(input[Symbol.asyncIterator]()), mode }
throw new TypeError("Unsupported input format.")
}

Expand Down
2 changes: 1 addition & 1 deletion src/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export function centralHeader(file: ZipEntryDescription & Metadata, offset: bigi
header.setUint16(30, zip64HeaderLength, true)
// useless disk fields = zero (4 bytes)
// useless attributes = zero (4 bytes)
header.setUint16(40, file.isFile ? 0o100664 : 0o040775, true) // UNIX regular file with permissions 664, or folder with permission 775.
header.setUint16(40, file.mode | (file.isFile ? 0o100000 : 0o040000), true)
header.setUint32(42, clampInt32(offset), true) // offset
return makeUint8Array(header)
}
Expand Down
4 changes: 2 additions & 2 deletions test/zip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const specDate = new Date("2019-04-26T02:00")
const invalidUTF8 = BufferFromHex("fe")

const baseFile: ZipFileDescription & Metadata = Object.freeze(
{ isFile: true, bytes: new Uint8Array(zipSpec), encodedName: specName, nameIsBuffer: false, modDate: specDate })
{ isFile: true, bytes: new Uint8Array(zipSpec), encodedName: specName, nameIsBuffer: false, modDate: specDate, mode: 0o664 })

const baseFolder: ZipFolderDescription & Metadata = Object.freeze(
{ isFile: false, encodedName: new TextEncoder().encode("folder"), nameIsBuffer: false, modDate: specDate })
{ isFile: false, encodedName: new TextEncoder().encode("folder"), nameIsBuffer: false, modDate: specDate, mode: 0o775 })

Deno.test("the ZIP fileHeader function makes file headers", () => {
const file = {...baseFile}
Expand Down