// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { isBrowser, isFileProvider, FileHandleFile } from '@loaders.gl/loader-utils'; import { makeZipCDHeaderIterator } from "../parse-zip/cd-file-header.js"; import { parseZipLocalFileHeader } from "../parse-zip/local-file-header.js"; import { DeflateCompression } from '@loaders.gl/compression'; import { IndexedArchive } from "./IndexedArchive.js"; /** Handling different compression types in zip */ export const ZIP_COMPRESSION_HANDLERS = { /** No compression */ 0: async (compressedFile) => compressedFile, /** Deflation */ 8: async (compressedFile) => { const compression = new DeflateCompression({ raw: true }); const decompressedData = await compression.decompress(compressedFile); return decompressedData; } }; /** * FileSystem adapter for a ZIP file * Holds FileProvider object that provides random access to archived files */ export class ZipFileSystem { /** FileProvider instance promise */ fileProvider = null; fileName; archive = null; /** * Constructor * @param file - instance of FileProvider or file path string */ constructor(file) { // Try to open file in NodeJS if (typeof file === 'string') { this.fileName = file; if (!isBrowser) { this.fileProvider = new FileHandleFile(file); } else { throw new Error('Cannot open file for random access in a WEB browser'); } } else if (file instanceof IndexedArchive) { this.fileProvider = file.fileProvider; this.archive = file; this.fileName = file.fileName; } else if (isFileProvider(file)) { this.fileProvider = file; } } /** Clean up resources */ async destroy() { if (this.fileProvider) { await this.fileProvider.destroy(); } } /** * Get file names list from zip archive * @returns array of file names */ async readdir() { if (!this.fileProvider) { throw new Error('No data detected in the zip archive'); } const fileNames = []; const zipCDIterator = makeZipCDHeaderIterator(this.fileProvider); for await (const cdHeader of zipCDIterator) { fileNames.push(cdHeader.fileName); } return fileNames; } /** * Get file metadata * @param filename - name of a file * @returns central directory data */ async stat(filename) { const cdFileHeader = await this.getCDFileHeader(filename); return { ...cdFileHeader, size: Number(cdFileHeader.uncompressedSize) }; } /** * Implementation of fetch against this file system * @param filename - name of a file * @returns - Response with file data */ async fetch(filename) { if (this.fileName && filename.indexOf(this.fileName) === 0) { filename = filename.substring(this.fileName.length + 1); } let uncompressedFile; if (this.archive) { uncompressedFile = await this.archive.getFile(filename, 'http'); } else { if (!this.fileProvider) { throw new Error('No data detected in the zip archive'); } const cdFileHeader = await this.getCDFileHeader(filename); const localFileHeader = await parseZipLocalFileHeader(cdFileHeader.localHeaderOffset, this.fileProvider); if (!localFileHeader) { throw new Error('Local file header has not been found in the zip archive`'); } const compressionHandler = ZIP_COMPRESSION_HANDLERS[localFileHeader.compressionMethod.toString()]; if (!compressionHandler) { throw Error('Only Deflation compression is supported'); } const compressedFile = await this.fileProvider.slice(localFileHeader.fileDataOffset, localFileHeader.fileDataOffset + localFileHeader.compressedSize); uncompressedFile = await compressionHandler(compressedFile); } const response = new Response(uncompressedFile); Object.defineProperty(response, 'url', { value: filename ? `${this.fileName || ''}/${filename}` : this.fileName || '' }); return response; } /** * Get central directory file header * @param filename - name of a file * @returns central directory file header */ async getCDFileHeader(filename) { if (!this.fileProvider) { throw new Error('No data detected in the zip archive'); } const zipCDIterator = makeZipCDHeaderIterator(this.fileProvider); let result = null; for await (const cdHeader of zipCDIterator) { if (cdHeader.fileName === filename) { result = cdHeader; break; } } if (!result) { throw new Error('File has not been found in the zip archive'); } return result; } }