import JSZip from "jszip";
import * as jsp from "jaspagate";
import * as adb from "./alabaster-abstract.js";
import * as afile from "./abstract/file.js";
class ZippedProjectNavigator {
#zipfile;
#ziphandle;
#prefix;
constructor(zipfile, ziphandle, prefix) {
this.#zipfile = zipfile;
this.#ziphandle = null;
this.#prefix = prefix;
}
async get(path, asBuffer) {
if (this.#ziphandle == null) {
this.#ziphandle = await JSZip.loadAsync(this.#zipfile.buffer());
}
// We always return a buffer.
return this.#ziphandle.file(jsp.joinPath(this.#prefix, path)).async("uint8array");
}
exists(path) {
return this.#ziphandle.file(jsp.joinPath(this.#prefix, path)) !== null;
}
clean(path) {}
};
/**
* Search a ZIP file for SummarizedExperiments to use in {@linkplain ZippedAlabasterDataset} or {@linkplain ZippedAlabasterResult}.
*
* @param {JSZip} handle - A handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package.
*
* @return {Map} Object where the keys are the paths/names of possible SummarizedExperiment objects,
* and each value is a 2-element array of dimensions.
*/
export async function searchZippedAlabaster(handle) {
let candidates = [];
for (const name of Object.keys(handle.files)) {
if (name == "OBJECT") {
candidates.push("");
} else if (name.endsWith("/OBJECT")) {
candidates.push(name.slice(0, name.length - 6));
}
}
candidates.sort();
const nonchildren = new Map;
let counter = 0;
while (counter < candidates.length) {
let prefix = candidates[counter];
counter++;
let contents = await handle.file(prefix + "OBJECT").async("string");
let meta = {};
try {
meta = JSON.parse(contents);
} catch(e) {
;
}
if ("summarized_experiment" in meta) {
nonchildren.set(prefix == "" ? "." : prefix.slice(0, prefix.length - 1), meta.summarized_experiment.dimensions);
while (counter < candidates.length) {
if (!candidates[counter].startsWith(prefix)) {
break;
}
counter++;
}
}
}
return nonchildren;
}
/************************
******* Dataset ********
************************/
/**
* Dataset as a ZIP file containing a SummarizedExperiment in the **alabaster** representation.
* Specifically, the ZIP file should contain the contents of an **alabaster** project directory.
* This project directory may contain multiple objects; the SummarizedExperiment of interest is identified in the constructor.
*
* @extends AbstractAlabasterDataset
*/
export class ZippedAlabasterDataset extends adb.AbstractAlabasterDataset {
#zipfile;
#prefix;
/**
* @param {string} prefix - Name of the SummarizedExperiment object inside the ZIP file.
* This should be `.` if the `OBJECT` file is stored at the root of the ZIP file,
* otherwise it should be the relative path to the object directory containing the `OBJECT` file.
* @param {SimpleFile|string|File} zipfile - Contents of the ZIP file containing the project directory.
* On browsers, this may be a File object.
* On Node.js, this may also be a string containing a file path.
* @param {object} [options={}] - Optional parameters.
* @param {?JSZip} [options.existingHandle=null] - An existing handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package.
* If an existing handle already exists, passing it in here will allow it to be re-used for greater efficiency.
* If `null`, a new handle is created for this ZippedAlabasterDataset instance.
*/
constructor(prefix, zipfile, options={}) {
let ziphandle = null;
if ("existingHandle" in options) {
ziphandle = options.existingHandle;
delete options.existingHandle;
} else {
if (!(zipfile instanceof afile.SimpleFile)) {
zipfile = new afile.SimpleFile(zipfile);
}
}
let nav = new ZippedProjectNavigator(zipfile, ziphandle, prefix);
super(nav);
this.#zipfile = zipfile;
this.#prefix = prefix;
}
/**
* @return {string} String specifying the format for this dataset.
*/
static format() {
return "alabaster-zipped";
}
#dump_summary(fun) {
let files = [ { type: "zip", file: fun(this.#zipfile) } ];
let opt = this.options();
opt.datasetPrefix = this.#prefix; // storing the name as a special option... can't be bothered to store it as a separate file.
return { files: files, options: opt };
}
/**
* @return {object} Object containing the abbreviated details of this dataset.
*/
abbreviate() {
return this.#dump_summary(f => {
return { size: f.size(), name: f.name() }
});
}
/**
* @return {object} Object describing this dataset, containing:
*
* - `files`: Array of objects representing the files used in this dataset.
* Each object corresponds to a single file and contains:
* - `type`: a string denoting the type.
* - `file`: a {@linkplain SimpleFile} object representing the file contents.
* - `options`: An object containing additional options to saved.
*/
serialize() {
return this.#dump_summary(f => f);
}
/**
* @param {Array} files - Array of objects like that produced by {@linkcode ZippedAlabasterDataset#serialize serialize}.
* @param {object} options - Object containing additional options to be passed to the constructor.
* @return {ZippedAlabasterDataset} A new instance of this class.
* @static
*/
static unserialize(files, options) {
if (files.length != 1 || files[0].type != "zip") {
throw new Error("expected exactly one file of type 'zip' for Zipped alabaster unserialization");
}
let prefix = options.datasetPrefix;
delete options.datasetPrefix;
let output = new ZippedAlabasterDataset(prefix, files[0].file);
output.setOptions(output);
return output;
}
}
/***********************
******* Result ********
***********************/
/**
* Result as a ZIP file containing a SummarizedExperiment in the **alabaster** representation,
* Specifically, the ZIP file should contain the contents of an **alabaster** project directory.
* This project directory may contain multiple objects; the SummarizedExperiment of interest is identified in the constructor.
*
* @extends AbstractAlabasterResult
*/
export class ZippedAlabasterResult extends adb.AbstractAlabasterResult {
/**
* @param {string} prefix - Name of the SummarizedExperiment object inside the project directory.
* @param {SimpleFile|string|File} zipfile - Contents of the ZIP file containing the project directory.
* On browsers, this may be a File object.
* On Node.js, this may also be a string containing a file path.
* @param {object} [options={}] - Optional parameters.
* @param {?JSZip} [options.existingHandle=null] - An existing handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package.
* If an existing handle already exists, passing it in here will allow it to be re-used for greater efficiency.
* If `null`, a new handle is created for this ZippedAlabasterDataset instance.
*/
constructor(prefix, zipfile, options={}) {
let ziphandle = null;
if ("existingHandle" in options) {
ziphandle = options.existingHandle;
delete options.existingHandle;
} else {
if (!(zipfile instanceof afile.SimpleFile)) {
zipfile = new afile.SimpleFile(zipfile);
}
}
let nav = new ZippedProjectNavigator(zipfile, ziphandle, prefix);
super(nav);
}
}