@loaders.gl/zip
Version:
Zip Archive Loader
141 lines (140 loc) • 5.17 kB
JavaScript
// 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;
}
}