aboutsummaryrefslogtreecommitdiff
path: root/src/server/ApiManagers/DownloadManager.ts
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-07-21 13:21:25 -0400
committerbobzel <zzzman@gmail.com>2025-07-21 13:21:25 -0400
commit3f489c64d9e55d452c255f8e2c10b0d754883dbb (patch)
tree187f6fd769fbb940dd5ddf24d91fbb51a5f6e629 /src/server/ApiManagers/DownloadManager.ts
parent401431d732fecd6bdef69d83e21253e1b157416d (diff)
more server side cleanup
Diffstat (limited to 'src/server/ApiManagers/DownloadManager.ts')
-rw-r--r--src/server/ApiManagers/DownloadManager.ts262
1 files changed, 0 insertions, 262 deletions
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
deleted file mode 100644
index 5ee21fb44..000000000
--- a/src/server/ApiManagers/DownloadManager.ts
+++ /dev/null
@@ -1,262 +0,0 @@
-import * as Archiver from 'archiver';
-import * as express from 'express';
-import * as path from 'path';
-import { URL } from 'url';
-import { DashUploadUtils, SizeSuffix } from '../DashUploadUtils';
-import { Method } from '../RouteManager';
-import RouteSubscriber from '../RouteSubscriber';
-import { Directory, publicDirectory, serverPathToFile } from '../SocketData';
-import { Database } from '../database';
-import ApiManager, { Registration } from './ApiManager';
-
-export type Hierarchy = { [id: string]: string | Hierarchy };
-export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
-export interface DocumentElements {
- data: string | any[];
- title: string;
-}
-
-/**
- * This is a very specific utility method to help traverse the database
- * to parse data and titles out of images and collections alone.
- *
- * We don't know if the document id given to is corresponds to a view document or a data
- * document. If it's a data document, the response from the database will have
- * a data field. If not, call recursively on the proto, and resolve with *its* data
- *
- * @param targetId the id of the Dash document whose data is being requests
- * @returns the data of the document, as well as its title
- */
-async function getData(targetId: string): Promise<DocumentElements> {
- return new Promise<DocumentElements>((resolve, reject) => {
- Database.Instance.getDocument(targetId, async (result: any) => {
- const { data, proto, title } = result.fields;
- if (data) {
- if (data.url) {
- resolve({ data: data.url, title });
- } else if (data.fields) {
- resolve({ data: data.fields, title });
- } else {
- reject();
- }
- } else if (proto) {
- getData(proto.fieldId).then(resolve, reject);
- } else {
- reject();
- }
- });
- });
-}
-
-/**
- * This function starts with a single document id as a seed,
- * typically that of a collection, and then descends the entire tree
- * of image or collection documents that are reachable from that seed.
- * @param seedId the id of the root of the subtree we're trying to capture, interesting only if it's a collection
- * @param hierarchy the data structure we're going to use to record the nesting of the collections and images as we descend
-
-Below is an example of the JSON hierarchy built from two images contained inside a collection titled 'a nested collection',
-following the general recursive structure shown immediately below
-{
- "parent folder name":{
- "first child's fild name":"first child's url"
- ...
- "nth child's fild name":"nth child's url"
- }
-}
-{
- "a nested collection (865c4734-c036-4d67-a588-c71bb43d1440)":{
- "an image of a cat (ace99ffd-8ed8-4026-a5d5-a353fff57bdd).jpg":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg",
- "1*SGJw31T5Q9Zfsk24l2yirg.gif (9321cc9b-9b3e-4cb6-b99c-b7e667340f05).gif":"https://cdn-media-1.freecodecamp.org/images/1*SGJw31T5Q9Zfsk24l2yirg.gif"
- }
-}
-*/
-async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Promise<void> {
- const { title, data } = await getData(seedId);
- const label = `${title} (${seedId})`;
- // is the document a collection?
- if (Array.isArray(data)) {
- // recurse over all documents in the collection.
- const local: Hierarchy = {}; // create a child hierarchy for this level, which will get passed in as the parent of the recursive call
- hierarchy[label] = local; // store it at the index in the parent, so we'll end up with a map of maps of maps
- await Promise.all(data.map(proxy => buildHierarchyRecursive(proxy.fieldId, local)));
- } else {
- // now, data can only be a string, namely the url of the image
- const filename = label + path.extname(data); // this is the file name under which the output image will be stored
- hierarchy[filename] = data;
- }
-}
-
-/**
- * This utility function factors out the process
- * of creating a zip file and sending it back to the client
- * by piping it into a response.
- *
- * Learn more about piping and readable / writable streams here!
- * https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be93/
- *
- * @param res the writable stream response object that will transfer the generated zip file
- * @param mutator the callback function used to actually modify and insert information into the zip instance
- */
-export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMutator): Promise<void> {
- res.set('Content-disposition', `attachment;`);
- res.set('Content-Type', 'application/zip');
- const zip = Archiver('zip');
- zip.pipe(res);
- await mutator(zip);
- return zip.finalize();
-}
-
-/**
- *
- * @param file the zip file to which we write the files
- * @param hierarchy the data structure from which we read, defining the nesting of the documents in the zip
- * @param prefix lets us create nested folders in the zip file by continually appending to the end
- * of the prefix with each layer of recursion.
- *
- * Function Call #1 => "Dash Export"
- * Function Call #2 => "Dash Export/a nested collection"
- * Function Call #3 => "Dash Export/a nested collection/lowest level collection"
- * ...
- */
-async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = 'Dash Export'): Promise<void> {
- // eslint-disable-next-line no-restricted-syntax
- for (const documentTitle in hierarchy) {
- if (Object.prototype.hasOwnProperty.call(hierarchy, documentTitle)) {
- const result = hierarchy[documentTitle];
- // base case or leaf node, we've hit a url (image)
- if (typeof result === 'string') {
- let fPath: string;
- const matches = /:\d+\/files\/images\/(upload_[\da-z]{32}.*)/g.exec(result);
- if (matches !== null) {
- // image already exists on our server
- fPath = serverPathToFile(Directory.images, matches[1]);
- } else {
- // the image doesn't already exist on our server (may have been dragged
- // and dropped in the browser and thus hosted remotely) so we upload it
- // to our server and point the zip file to it, so it can bundle up the bytes
- // eslint-disable-next-line no-await-in-loop
- const information = await DashUploadUtils.UploadImage(result);
- fPath = information instanceof Error ? '' : information.accessPaths[SizeSuffix.Original].server;
- }
- // write the file specified by the path to the directory in the
- // zip file given by the prefix.
- if (fPath) {
- file.file(fPath, { name: documentTitle, prefix });
- }
- } else {
- // we've hit a collection, so we have to recurse
- // eslint-disable-next-line no-await-in-loop
- await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
- }
- }
- }
-}
-
-async function getDocs(docId: string) {
- const files = new Set<string>();
- const docs: { [id: string]: any } = {};
- const fn = (doc: any): string[] => {
- const { id } = doc;
- if (typeof id === 'string' && id.endsWith('Proto')) {
- // Skip protos
- return [];
- }
- const ids: string[] = [];
- // eslint-disable-next-line no-restricted-syntax
- for (const key in doc.fields) {
- // eslint-disable-next-line no-continue
- if (!Object.prototype.hasOwnProperty.call(doc.fields, key)) continue;
-
- const field = doc.fields[key];
- // eslint-disable-next-line no-continue
- if (field === undefined || field === null) continue;
-
- if (field.__type === 'proxy' || field.__type === 'prefetch_proxy') {
- ids.push(field.fieldId);
- } else if (field.__type === 'script' || field.__type === 'computed') {
- field.captures && ids.push(field.captures.fieldId);
- } else if (field.__type === 'list') {
- ids.push(...fn(field));
- } else if (typeof field === 'string') {
- const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w-]*)"/g;
- for (let match = re.exec(field); match !== null; match = re.exec(field)) {
- ids.push(match[1]);
- }
- } else if (field.__type === 'RichTextField') {
- const re = /"href"\s*:\s*"(.*?)"/g;
- for (let match = re.exec(field.data); match !== null; match = re.exec(field.Data)) {
- const urlString = match[1];
- const split = new URL(urlString).pathname.split('doc/');
- if (split.length > 1) {
- ids.push(split[split.length - 1]);
- }
- }
- const re2 = /"src"\s*:\s*"(.*?)"/g;
- for (let match = re2.exec(field.Data); match !== null; match = re2.exec(field.Data)) {
- const urlString = match[1];
- const { pathname } = new URL(urlString);
- files.add(pathname);
- }
- } else if (['audio', 'image', 'video', 'pdf', 'web', 'map'].includes(field.__type)) {
- const { pathname } = new URL(field.url);
- files.add(pathname);
- }
- }
-
- if (doc.id) {
- docs[doc.id] = doc;
- }
- return ids;
- };
- await Database.Instance.visit([docId], fn);
- return { id: docId, docs, files };
-}
-
-export default class DownloadManager extends ApiManager {
- protected initialize(register: Registration): void {
- /**
- * Let's say someone's using Dash to organize images in collections.
- * This lets them export the hierarchy they've built to their
- * own file system in a useful format.
- *
- * This handler starts with a single document id (interesting only
- * if it's that of a collection). It traverses the database, captures
- * the nesting of only nested images or collections, writes
- * that to a zip file and returns it to the client for download.
- */
- register({
- method: Method.GET,
- subscription: new RouteSubscriber('imageHierarchyExport').add('docId'),
- secureHandler: async ({ req, res }) => {
- const id = req.params.docId;
- const hierarchy: Hierarchy = {};
- await buildHierarchyRecursive(id, hierarchy);
- return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy));
- },
- });
-
- register({
- method: Method.GET,
- subscription: new RouteSubscriber('downloadId').add('docId'),
- secureHandler: async ({ req, res }) =>
- BuildAndDispatchZip(res, async zip => {
- const { id, docs, files } = await getDocs(req.params.docId);
- const docString = JSON.stringify({ id, docs });
- zip.append(docString, { name: 'doc.json' });
- files.forEach(val => {
- zip.file(publicDirectory + val, { name: val.substring(1) });
- });
- }),
- });
-
- register({
- method: Method.GET,
- subscription: new RouteSubscriber('serializeDoc').add('docId'),
- secureHandler: async ({ req, res }) => {
- const { docs, files } = await getDocs(req.params.docId);
- res.send({ docs, files: Array.from(files) });
- },
- });
- }
-}