readers/ArtifactDB-zipped.js

  1. import * as adb from "./ArtifactDB-abstract.js";
  2. import JSZip from "jszip";
  3. import * as afile from "./abstract/file.js";
  4. class ZippedProjectNavigator {
  5. #zipfile;
  6. #ziphandle;
  7. constructor(zipfile, ziphandle) {
  8. this.#zipfile = zipfile;
  9. this.#ziphandle = null;
  10. }
  11. async file(path) {
  12. if (this.#ziphandle == null) {
  13. this.#ziphandle = await JSZip.loadAsync(this.#zipfile.buffer());
  14. }
  15. return await this.#ziphandle.file(path).async("uint8array");
  16. }
  17. async metadata(path) {
  18. if (this.#ziphandle == null) {
  19. this.#ziphandle = await JSZip.loadAsync(this.#zipfile.buffer());
  20. }
  21. while (1) {
  22. if (!path.endsWith(".json")) {
  23. path += ".json";
  24. }
  25. let contents = await this.#ziphandle.file(path).async("string");
  26. let values = JSON.parse(contents);
  27. if (values["$schema"].startsWith("redirection/")){
  28. path = values.redirection.targets[0].location;
  29. } else {
  30. return values;
  31. }
  32. }
  33. }
  34. clear() {
  35. this.#ziphandle = null;
  36. }
  37. };
  38. /**
  39. * Search a ZIP file for SummarizedExperiments to use in {@linkplain ZippedArtifactdbDataset} or {@linkplain ZippedArtifactdbResult}.
  40. *
  41. * @param {JSZip} handle - A handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package.
  42. *
  43. * @return {Map} Object where the keys are the paths/names of possible SummarizedExperiment objects,
  44. * and each value is a 2-element array of dimensions.
  45. */
  46. export async function searchZippedArtifactdb(handle) {
  47. // Sorting by the number of slashes.
  48. let all_json = [];
  49. for (const name of Object.keys(handle.files)) {
  50. if (name.endsWith(".json")) {
  51. all_json.push({ name: name, path: name.split("/") });
  52. }
  53. }
  54. all_json.sort((a, b) => a.path.length - b.path.length);
  55. let found_se = new Map;
  56. let nonchildren = new Map;
  57. let redirects = new Map;
  58. for (const x of all_json) {
  59. // Avoid loading JSONs for files in subdirectories of known SEs.
  60. let current = found_se;
  61. let already_found = false;
  62. for (const comp of x.path) {
  63. let val = current.get(comp);
  64. if (typeof val === "undefined") {
  65. val = new Map;
  66. current.set(comp, val);
  67. } else if (val === null) {
  68. already_found = true;
  69. break;
  70. }
  71. current = val;
  72. }
  73. // Otherwise, we load it in and peel out some information.
  74. if (!already_found) {
  75. let contents = await handle.file(x.name).async("string");
  76. let values = JSON.parse(contents);
  77. if ("summarized_experiment" in values) {
  78. nonchildren.set(values.path, values.summarized_experiment.dimensions);
  79. } else if (values["$schema"].startsWith("redirection/")) {
  80. redirects.set(values.path, values.redirection.targets[0].location);
  81. }
  82. }
  83. }
  84. for (const [rr, loc] of redirects) {
  85. let found = nonchildren.get(loc);
  86. if (typeof found !== "undefined") {
  87. nonchildren.delete(loc);
  88. nonchildren.set(rr, found);
  89. }
  90. }
  91. return nonchildren;
  92. }
  93. /************************
  94. ******* Dataset ********
  95. ************************/
  96. /**
  97. * Dataset as a ZIP file containing a SummarizedExperiment in the **ArtifactDB** representation,
  98. * e.g., as produced by {@linkcode saveSingleCellExperiment}.
  99. * Specifically, the ZIP file should contain the contents of an **ArtifactDB** project directory.
  100. * This project directory may contain multiple objects; the SummarizedExperiment of interest is identified in the constructor.
  101. *
  102. * @extends AbstractArtifactdbDataset
  103. */
  104. export class ZippedArtifactdbDataset extends adb.AbstractArtifactdbDataset {
  105. #zipfile;
  106. #name;
  107. /**
  108. * @param {string} name - Name of the SummarizedExperiment object inside the project directory.
  109. * @param {SimpleFile|string|File} zipfile - Contents of the ZIP file containing the project directory.
  110. * On browsers, this may be a File object.
  111. * On Node.js, this may also be a string containing a file path.
  112. * @param {object} [options={}] - Optional parameters.
  113. * @param {?JSZip} [options.existingHandle=null] - An existing handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package.
  114. * If an existing handle already exists, passing it in here will allow it to be re-used for greater efficiency.
  115. * If `null`, a new handle is created for this ZippedArtifactdbDataset instance.
  116. */
  117. constructor(name, zipfile, options={}) {
  118. let ziphandle = null;
  119. if ("existingHandle" in options) {
  120. ziphandle = options.existingHandle;
  121. delete options.existingHandle;
  122. } else {
  123. if (!(zipfile instanceof afile.SimpleFile)) {
  124. zipfile = new afile.SimpleFile(zipfile);
  125. }
  126. }
  127. let nav = new ZippedProjectNavigator(zipfile, ziphandle);
  128. super(name, nav);
  129. this.#zipfile = zipfile;
  130. this.#name = name;
  131. }
  132. /**
  133. * @return {string} String specifying the format for this dataset.
  134. */
  135. static format() {
  136. return "ArtifactDB-zipped";
  137. }
  138. #dump_summary(fun) {
  139. let files = [ { type: "zip", file: fun(this.#zipfile) } ];
  140. let opt = this.options();
  141. opt.datasetName = this.#name; // storing the name as a special option... can't be bothered to store it as a separate file.
  142. return { files: files, options: opt };
  143. }
  144. /**
  145. * @return {object} Object containing the abbreviated details of this dataset.
  146. */
  147. abbreviate() {
  148. return this.#dump_summary(f => {
  149. return { size: f.size(), name: f.name() }
  150. });
  151. }
  152. /**
  153. * @return {object} Object describing this dataset, containing:
  154. *
  155. * - `files`: Array of objects representing the files used in this dataset.
  156. * Each object corresponds to a single file and contains:
  157. * - `type`: a string denoting the type.
  158. * - `file`: a {@linkplain SimpleFile} object representing the file contents.
  159. * - `options`: An object containing additional options to saved.
  160. */
  161. serialize() {
  162. return this.#dump_summary(f => f);
  163. }
  164. /**
  165. * @param {Array} files - Array of objects like that produced by {@linkcode ZippedArtifactdbDataset#serialize serialize}.
  166. * @param {object} options - Object containing additional options to be passed to the constructor.
  167. * @return {ZippedArtifactdbDataset} A new instance of this class.
  168. * @static
  169. */
  170. static unserialize(files, options) {
  171. if (files.length != 1 || files[0].type != "zip") {
  172. throw new Error("expected exactly one file of type 'zip' for Zipped ArtifactDB unserialization");
  173. }
  174. let name = options.datasetName;
  175. delete options.datasetName;
  176. let output = new ZippedArtifactdbDataset(name, files[0].file);
  177. output.setOptions(output);
  178. return output;
  179. }
  180. }
  181. /***********************
  182. ******* Result ********
  183. ***********************/
  184. /**
  185. * Result as a ZIP file containing a SummarizedExperiment in the **ArtifactDB** representation,
  186. * e.g., as produced by {@linkcode saveSingleCellExperiment}.
  187. * Specifically, the ZIP file should contain the contents of an **ArtifactDB** project directory.
  188. * This project directory may contain multiple objects; the SummarizedExperiment of interest is identified in the constructor.
  189. *
  190. * @extends AbstractArtifactdbResult
  191. */
  192. export class ZippedArtifactdbResult extends adb.AbstractArtifactdbResult {
  193. /**
  194. * @param {string} name - Name of the SummarizedExperiment object inside the project directory.
  195. * @param {SimpleFile|string|File} zipfile - Contents of the ZIP file containing the project directory.
  196. * On browsers, this may be a File object.
  197. * On Node.js, this may also be a string containing a file path.
  198. * @param {object} [options={}] - Optional parameters.
  199. * @param {?JSZip} [options.existingHandle=null] - An existing handle into the ZIP file, generated using the [**JSZip**](https://stuk.github.io/jszip/) package.
  200. * If an existing handle already exists, passing it in here will allow it to be re-used for greater efficiency.
  201. * If `null`, a new handle is created for this ZippedArtifactdbDataset instance.
  202. */
  203. constructor(name, zipfile, options={}) {
  204. let ziphandle = null;
  205. if ("existingHandle" in options) {
  206. ziphandle = options.existingHandle;
  207. delete options.existingHandle;
  208. } else {
  209. if (!(zipfile instanceof afile.SimpleFile)) {
  210. zipfile = new afile.SimpleFile(zipfile);
  211. }
  212. }
  213. let nav = new ZippedProjectNavigator(zipfile, ziphandle);
  214. super(name, nav);
  215. }
  216. }