diff --git a/src/mol-plugin/state/actions/data-format.ts b/src/mol-plugin/state/actions/data-format.ts index 0eb77a0e7e8baf2099b2e656e27c63fcde7c860c..75ae09650b465dcce5012b5bbac8cd058b7a3c5b 100644 --- a/src/mol-plugin/state/actions/data-format.ts +++ b/src/mol-plugin/state/actions/data-format.ts @@ -125,7 +125,7 @@ export const OpenFile = StateAction.build({ params: (a, ctx: PluginContext) => { const { extensions, options } = ctx.dataFormat.registry return { - file: PD.File({ accept: Array.from(extensions.values()).map(e => `.${e}`).join(',')}), + file: PD.File({ accept: Array.from(extensions.values()).map(e => `.${e}`).join(',') + ',.gz,.zip' }), format: PD.Select('auto', options), visuals: PD.Boolean(true, { description: 'Add default visuals' }), } diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts index 5a0860c49fe18b175c52f725b9fc9111b10904b3..6a8fe0c47216fbf57ef7e2e4d069800ccf80f125 100644 --- a/src/mol-util/data-source.ts +++ b/src/mol-util/data-source.ts @@ -8,6 +8,9 @@ */ import { Task, RuntimeContext } from '../mol-task'; +import { parse, ungzip } from './zip/zip'; +// import { inflate, inflateRaw, parse } from 'uzip-module'; +import { utf8Read } from '../mol-io/common/utf8'; // polyfill XMLHttpRequest in node.js const XHR = typeof document === 'undefined' ? require('xhr2') as { @@ -20,6 +23,12 @@ const XHR = typeof document === 'undefined' ? require('xhr2') as { readonly UNSENT: number; } : XMLHttpRequest +export enum DataCompressionMethod { + None, + Gzip, + Zip, +} + type DataType = 'json' | 'xml' | 'string' | 'binary' type DataValue = 'string' | any | XMLDocument | Uint8Array type DataResponse<T extends DataType> = @@ -105,18 +114,49 @@ function readData<T extends XMLHttpRequest | FileReader>(ctx: RuntimeContext, ac }); } -function processFile<T extends DataType>(reader: FileReader, type: T): DataResponse<T> { +function getCompression(name: string) { + return /\.gz$/i.test(name) ? DataCompressionMethod.Gzip : + /\.zip$/i.test(name) ? DataCompressionMethod.Zip : + DataCompressionMethod.None +} + +function decompress(data: Uint8Array, compression: DataCompressionMethod): Uint8Array { + switch (compression) { + case DataCompressionMethod.None: return data + case DataCompressionMethod.Gzip: return ungzip(data) + case DataCompressionMethod.Zip: + const parsed = parse(data.buffer) + const names = Object.keys(parsed) + if (names.length !== 1) throw new Error('can only decompress zip files with a single entry') + return parsed[names[0]] as Uint8Array + } +} + +function processFile<T extends DataType>(reader: FileReader, type: T, compression: DataCompressionMethod): DataResponse<T> { const { result } = reader - if (type === 'binary' && result instanceof ArrayBuffer) { - return new Uint8Array(result) as DataResponse<T> - } else if (type === 'string' && typeof result === 'string') { - return result as DataResponse<T> - } else if (type === 'xml' && typeof result === 'string') { + let data = result instanceof ArrayBuffer ? new Uint8Array(result) : result + if (data === null) throw new Error('no data given') + + if (compression !== DataCompressionMethod.None) { + if (!(data instanceof Uint8Array)) throw new Error('need Uint8Array for decompression') + const decompressed = decompress(data, compression); + if (type === 'string') { + data = utf8Read(decompressed, 0, decompressed.length); + } else { + data = decompressed + } + } + + if (type === 'binary' && data instanceof Uint8Array) { + return data as DataResponse<T> + } else if (type === 'string' && typeof data === 'string') { + return data as DataResponse<T> + } else if (type === 'xml' && typeof data === 'string') { const parser = new DOMParser(); - return parser.parseFromString(result, 'application/xml') as DataResponse<T> - } else if (type === 'json' && typeof result === 'string') { - return JSON.parse(result) as DataResponse<T> + return parser.parseFromString(data, 'application/xml') as DataResponse<T> + } else if (type === 'json' && typeof data === 'string') { + return JSON.parse(data) as DataResponse<T> } throw new Error(`could not get requested response data '${type}'`) } @@ -126,15 +166,19 @@ function readFromFileInternal<T extends DataType>(file: File, type: T): Task<Dat return Task.create('Read File', async ctx => { try { reader = new FileReader(); + const compression = getCompression(file.name) - if (type === 'binary') reader.readAsArrayBuffer(file) - else reader.readAsText(file) + if (type === 'binary' || compression !== DataCompressionMethod.None) { + reader.readAsArrayBuffer(file) + } else { + reader.readAsText(file) + } await ctx.update({ message: 'Opening file...', canAbort: true }); const fileReader = await readData(ctx, 'Reading...', reader); await ctx.update({ message: 'Parsing file...', canAbort: false }); - return processFile(fileReader, type); + return processFile(fileReader, type, compression); } finally { reader = void 0; }