aboutsummaryrefslogtreecommitdiff
path: root/src/server/ApiManagers
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/ApiManagers')
-rw-r--r--src/server/ApiManagers/DeleteManager.ts36
-rw-r--r--src/server/ApiManagers/GooglePhotosManager.ts621
-rw-r--r--src/server/ApiManagers/PDFManager.ts116
-rw-r--r--src/server/ApiManagers/UploadManager.ts130
-rw-r--r--src/server/ApiManagers/UserManager.ts25
5 files changed, 384 insertions, 544 deletions
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index 46c0d8a8a..c6c4ca464 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -1,21 +1,19 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method, _permission_denied } from "../RouteManager";
-import { WebSocket } from "../websocket";
-import { Database } from "../database";
-import rimraf = require("rimraf");
-import { filesDirectory } from "..";
-import { DashUploadUtils } from "../DashUploadUtils";
-import { mkdirSync } from "fs";
-import RouteSubscriber from "../RouteSubscriber";
+import ApiManager, { Registration } from './ApiManager';
+import { Method, _permission_denied } from '../RouteManager';
+import { WebSocket } from '../websocket';
+import { Database } from '../database';
+import { rimraf } from 'rimraf';
+import { filesDirectory } from '..';
+import { DashUploadUtils } from '../DashUploadUtils';
+import { mkdirSync } from 'fs';
+import RouteSubscriber from '../RouteSubscriber';
export default class DeleteManager extends ApiManager {
-
protected initialize(register: Registration): void {
-
register({
method: Method.GET,
requireAdminInRelease: true,
- subscription: new RouteSubscriber("delete").add("target?"),
+ subscription: new RouteSubscriber('delete').add('target?'),
secureHandler: async ({ req, res }) => {
const { target } = req.params;
@@ -24,12 +22,12 @@ export default class DeleteManager extends ApiManager {
} else {
let all = false;
switch (target) {
- case "all":
+ case 'all':
all = true;
- case "database":
+ case 'database':
await WebSocket.doDelete(false);
if (!all) break;
- case "files":
+ case 'files':
rimraf.sync(filesDirectory);
mkdirSync(filesDirectory);
await DashUploadUtils.buildFileDirectories();
@@ -39,10 +37,8 @@ export default class DeleteManager extends ApiManager {
}
}
- res.redirect("/home");
- }
+ res.redirect('/home');
+ },
});
-
}
-
-} \ No newline at end of file
+}
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index be17b698e..5feb25fd4 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -1,331 +1,324 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method, _error, _success, _invalid } from "../RouteManager";
-import * as path from "path";
-import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
-import { BatchedArray, TimeUnit } from "array-batcher";
-import { Opt } from "../../fields/Doc";
-import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
-import { Database } from "../database";
-import { red } from "colors";
-import { Upload } from "../SharedMediaTypes";
-import request = require('request-promise');
-import { NewMediaItemResult } from "../apis/google/SharedTypes";
+// import ApiManager, { Registration } from './ApiManager';
+// import { Method, _error, _success, _invalid } from '../RouteManager';
+// import * as path from 'path';
+// import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils';
+// import { BatchedArray, TimeUnit } from 'array-batcher';
+// import { Opt } from '../../fields/Doc';
+// import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
+// import { Database } from '../database';
+// import { red } from 'colors';
+// import { Upload } from '../SharedMediaTypes';
+// import * as request from 'request-promise';
+// import { NewMediaItemResult } from '../apis/google/SharedTypes';
-const prefix = "google_photos_";
-const remoteUploadError = "None of the preliminary uploads to Google's servers was successful.";
-const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
-const mediaError = "Unable to convert all uploaded bytes to media items!";
-const localUploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
-const requestError = "Unable to execute download: the body's media items were malformed.";
-const downloadError = "Encountered an error while executing downloads.";
+// const prefix = 'google_photos_';
+// const remoteUploadError = "None of the preliminary uploads to Google's servers was successful.";
+// const authenticationError = 'Unable to authenticate Google credentials before uploading to Google Photos!';
+// const mediaError = 'Unable to convert all uploaded bytes to media items!';
+// const localUploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
+// const requestError = "Unable to execute download: the body's media items were malformed.";
+// const downloadError = 'Encountered an error while executing downloads.';
-interface GooglePhotosUploadFailure {
- batch: number;
- index: number;
- url: string;
- reason: string;
-}
+// interface GooglePhotosUploadFailure {
+// batch: number;
+// index: number;
+// url: string;
+// reason: string;
+// }
-interface MediaItem {
- baseUrl: string;
-}
+// interface MediaItem {
+// baseUrl: string;
+// }
-interface NewMediaItem {
- description: string;
- simpleMediaItem: {
- uploadToken: string;
- };
-}
+// interface NewMediaItem {
+// description: string;
+// simpleMediaItem: {
+// uploadToken: string;
+// };
+// }
-/**
- * This manager handles the creation of routes for google photos functionality.
- */
-export default class GooglePhotosManager extends ApiManager {
+// /**
+// * This manager handles the creation of routes for google photos functionality.
+// */
+// export default class GooglePhotosManager extends ApiManager {
+// protected initialize(register: Registration): void {
+// /**
+// * This route receives a list of urls that point to images stored
+// * on Dash's file system, and, in a two step process, uploads them to Google's servers and
+// * returns the information Google generates about the associated uploaded remote images.
+// */
+// register({
+// method: Method.POST,
+// subscription: '/googlePhotosMediaPost',
+// secureHandler: async ({ user, req, res }) => {
+// const { media } = req.body;
- protected initialize(register: Registration): void {
+// // first we need to ensure that we know the google account to which these photos will be uploaded
+// const token = (await GoogleApiServerUtils.retrieveCredentials(user.id))?.credentials?.access_token;
+// if (!token) {
+// return _error(res, authenticationError);
+// }
- /**
- * This route receives a list of urls that point to images stored
- * on Dash's file system, and, in a two step process, uploads them to Google's servers and
- * returns the information Google generates about the associated uploaded remote images.
- */
- register({
- method: Method.POST,
- subscription: "/googlePhotosMediaPost",
- secureHandler: async ({ user, req, res }) => {
- const { media } = req.body;
+// // next, having one large list or even synchronously looping over things trips a threshold
+// // set on Google's servers, and would instantly return an error. So, we ease things out and send the photos to upload in
+// // batches of 25, where the next batch is sent 100 millieconds after we receive a response from Google's servers.
+// const failed: GooglePhotosUploadFailure[] = [];
+// const batched = BatchedArray.from<Uploader.UploadSource>(media, { batchSize: 25 });
+// const interval = { magnitude: 100, unit: TimeUnit.Milliseconds };
+// const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(interval, async (batch, collector, { completedBatches }) => {
+// for (let index = 0; index < batch.length; index++) {
+// const { url, description } = batch[index];
+// // a local function used to record failure of an upload
+// const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
+// // see image resizing - we store the size-agnostic url in our logic, but write out size-suffixed images to the file system
+// // so here, given a size agnostic url, we're just making that conversion so that the file system knows which bytes to actually upload
+// const imageToUpload = InjectSize(url, SizeSuffix.Original);
+// // STEP 1/2: send the raw bytes of the image from our server to Google's servers. We'll get back an upload token
+// // which acts as a pointer to those bytes that we can use to locate them later on
+// const uploadToken = await Uploader.SendBytes(token, imageToUpload).catch(fail);
+// if (!uploadToken) {
+// fail(`${path.extname(url)} is not an accepted extension`);
+// } else {
+// // gather the upload token return from Google (a pointer they give us to the raw, currently useless bytes
+// // we've uploaded to their servers) and put in the JSON format that the API accepts for image creation (used soon, below)
+// collector.push({
+// description,
+// simpleMediaItem: { uploadToken },
+// });
+// }
+// }
+// });
- // first we need to ensure that we know the google account to which these photos will be uploaded
- const token = (await GoogleApiServerUtils.retrieveCredentials(user.id))?.credentials?.access_token;
- if (!token) {
- return _error(res, authenticationError);
- }
+// // inform the developer / server console of any failed upload attempts
+// // does not abort the operation, since some subset of the uploads may have been successful
+// const { length } = failed;
+// if (length) {
+// console.error(`Unable to upload ${length} image${length === 1 ? '' : 's'} to Google's servers`);
+// console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
+// }
- // next, having one large list or even synchronously looping over things trips a threshold
- // set on Google's servers, and would instantly return an error. So, we ease things out and send the photos to upload in
- // batches of 25, where the next batch is sent 100 millieconds after we receive a response from Google's servers.
- const failed: GooglePhotosUploadFailure[] = [];
- const batched = BatchedArray.from<Uploader.UploadSource>(media, { batchSize: 25 });
- const interval = { magnitude: 100, unit: TimeUnit.Milliseconds };
- const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
- interval,
- async (batch, collector, { completedBatches }) => {
- for (let index = 0; index < batch.length; index++) {
- const { url, description } = batch[index];
- // a local function used to record failure of an upload
- const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
- // see image resizing - we store the size-agnostic url in our logic, but write out size-suffixed images to the file system
- // so here, given a size agnostic url, we're just making that conversion so that the file system knows which bytes to actually upload
- const imageToUpload = InjectSize(url, SizeSuffix.Original);
- // STEP 1/2: send the raw bytes of the image from our server to Google's servers. We'll get back an upload token
- // which acts as a pointer to those bytes that we can use to locate them later on
- const uploadToken = await Uploader.SendBytes(token, imageToUpload).catch(fail);
- if (!uploadToken) {
- fail(`${path.extname(url)} is not an accepted extension`);
- } else {
- // gather the upload token return from Google (a pointer they give us to the raw, currently useless bytes
- // we've uploaded to their servers) and put in the JSON format that the API accepts for image creation (used soon, below)
- collector.push({
- description,
- simpleMediaItem: { uploadToken }
- });
- }
- }
- }
- );
+// // if none of the preliminary uploads was successful, no need to try and create images
+// // report the failure to the client and return
+// if (!newMediaItems.length) {
+// console.error(red(`${remoteUploadError} Thus, aborting image creation. Please try again.`));
+// _error(res, remoteUploadError);
+// return;
+// }
- // inform the developer / server console of any failed upload attempts
- // does not abort the operation, since some subset of the uploads may have been successful
- const { length } = failed;
- if (length) {
- console.error(`Unable to upload ${length} image${length === 1 ? "" : "s"} to Google's servers`);
- console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
- }
+// // STEP 2/2: create the media items and return the API's response to the client, along with any failures
+// return Uploader.CreateMediaItems(token, newMediaItems, req.body.album).then(
+// results => _success(res, { results, failed }),
+// error => _error(res, mediaError, error)
+// );
+// },
+// });
- // if none of the preliminary uploads was successful, no need to try and create images
- // report the failure to the client and return
- if (!newMediaItems.length) {
- console.error(red(`${remoteUploadError} Thus, aborting image creation. Please try again.`));
- _error(res, remoteUploadError);
- return;
- }
+// /**
+// * This route receives a list of urls that point to images
+// * stored on Google's servers and (following a *rough* heuristic)
+// * uploads each image to Dash's server if it hasn't already been uploaded.
+// * Unfortunately, since Google has so many of these images on its servers,
+// * these user content urls expire every 6 hours. So we can't store the url of a locally uploaded
+// * Google image and compare the candidate url to it to figure out if we already have it,
+// * since the same bytes on their server might now be associated with a new, random url.
+// * So, we do the next best thing and try to use an intrinsic attribute of those bytes as
+// * an identifier: the precise content size. This works in small cases, but has the obvious flaw of failing to upload
+// * an image locally if we already have uploaded another Google user content image with the exact same content size.
+// */
+// register({
+// method: Method.POST,
+// subscription: '/googlePhotosMediaGet',
+// secureHandler: async ({ req, res }) => {
+// const { mediaItems } = req.body as { mediaItems: MediaItem[] };
+// if (!mediaItems) {
+// // non-starter, since the input was in an invalid format
+// _invalid(res, requestError);
+// return;
+// }
+// let failed = 0;
+// const completed: Opt<Upload.ImageInformation>[] = [];
+// for (const { baseUrl } of mediaItems) {
+// // start by getting the content size of the remote image
+// const results = await DashUploadUtils.InspectImage(baseUrl);
+// if (results instanceof Error) {
+// // if something went wrong here, we can't hope to upload it, so just move on to the next
+// failed++;
+// continue;
+// }
+// const { contentSize, ...attributes } = results;
+// // check to see if we have uploaded a Google user content image *specifically via this route* already
+// // that has this exact content size
+// const found: Opt<Upload.ImageInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize);
+// if (!found) {
+// // if we haven't, then upload it locally to Dash's server
+// const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, undefined, prefix, false).catch(error => _error(res, downloadError, error));
+// if (upload) {
+// completed.push(upload);
+// // inform the heuristic that we've encountered an image with this content size,
+// // to be later checked against in future uploads
+// await Database.Auxiliary.LogUpload(upload);
+// } else {
+// // make note of a failure to upload locallys
+// failed++;
+// }
+// } else {
+// // if we have, the variable 'found' is handily the upload information of the
+// // existing image, so we add it to the list as if we had just uploaded it now without actually
+// // making a duplicate write
+// completed.push(found);
+// }
+// }
+// // if there are any failures, report a general failure to the client
+// if (failed) {
+// return _error(res, localUploadError(failed));
+// }
+// // otherwise, return the image upload information list corresponding to the newly (or previously)
+// // uploaded images
+// _success(res, completed);
+// },
+// });
+// }
+// }
- // STEP 2/2: create the media items and return the API's response to the client, along with any failures
- return Uploader.CreateMediaItems(token, newMediaItems, req.body.album).then(
- results => _success(res, { results, failed }),
- error => _error(res, mediaError, error)
- );
- }
- });
+// /**
+// * This namespace encompasses the logic
+// * necessary to upload images to Google's server,
+// * and then initialize / create those images in the Photos
+// * API given the upload tokens returned from the initial
+// * uploading process.
+// *
+// * https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate
+// */
+// export namespace Uploader {
+// /**
+// * Specifies the structure of the object
+// * necessary to upload bytes to Google's servers.
+// * The url is streamed to access the image's bytes,
+// * and the description is what appears in Google Photos'
+// * description field.
+// */
+// export interface UploadSource {
+// url: string;
+// description: string;
+// }
- /**
- * This route receives a list of urls that point to images
- * stored on Google's servers and (following a *rough* heuristic)
- * uploads each image to Dash's server if it hasn't already been uploaded.
- * Unfortunately, since Google has so many of these images on its servers,
- * these user content urls expire every 6 hours. So we can't store the url of a locally uploaded
- * Google image and compare the candidate url to it to figure out if we already have it,
- * since the same bytes on their server might now be associated with a new, random url.
- * So, we do the next best thing and try to use an intrinsic attribute of those bytes as
- * an identifier: the precise content size. This works in small cases, but has the obvious flaw of failing to upload
- * an image locally if we already have uploaded another Google user content image with the exact same content size.
- */
- register({
- method: Method.POST,
- subscription: "/googlePhotosMediaGet",
- secureHandler: async ({ req, res }) => {
- const { mediaItems } = req.body as { mediaItems: MediaItem[] };
- if (!mediaItems) {
- // non-starter, since the input was in an invalid format
- _invalid(res, requestError);
- return;
- }
- let failed = 0;
- const completed: Opt<Upload.ImageInformation>[] = [];
- for (const { baseUrl } of mediaItems) {
- // start by getting the content size of the remote image
- const results = await DashUploadUtils.InspectImage(baseUrl);
- if (results instanceof Error) {
- // if something went wrong here, we can't hope to upload it, so just move on to the next
- failed++;
- continue;
- }
- const { contentSize, ...attributes } = results;
- // check to see if we have uploaded a Google user content image *specifically via this route* already
- // that has this exact content size
- const found: Opt<Upload.ImageInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize);
- if (!found) {
- // if we haven't, then upload it locally to Dash's server
- const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, undefined, prefix, false).catch(error => _error(res, downloadError, error));
- if (upload) {
- completed.push(upload);
- // inform the heuristic that we've encountered an image with this content size,
- // to be later checked against in future uploads
- await Database.Auxiliary.LogUpload(upload);
- } else {
- // make note of a failure to upload locallys
- failed++;
- }
- } else {
- // if we have, the variable 'found' is handily the upload information of the
- // existing image, so we add it to the list as if we had just uploaded it now without actually
- // making a duplicate write
- completed.push(found);
- }
- }
- // if there are any failures, report a general failure to the client
- if (failed) {
- return _error(res, localUploadError(failed));
- }
- // otherwise, return the image upload information list corresponding to the newly (or previously)
- // uploaded images
- _success(res, completed);
- }
- });
+// /**
+// * This is the format needed to pass
+// * into the BatchCreate API request
+// * to take a reference to raw uploaded bytes
+// * and actually create an image in Google Photos.
+// *
+// * So, to instantiate this interface you must have already dispatched an upload
+// * and received an upload token.
+// */
+// export interface NewMediaItem {
+// description: string;
+// simpleMediaItem: {
+// uploadToken: string;
+// };
+// }
- }
-}
+// /**
+// * A utility function to streamline making
+// * calls to the API's url - accentuates
+// * the relative path in the caller.
+// * @param extension the desired
+// * subset of the API
+// */
+// function prepend(extension: string): string {
+// return `https://photoslibrary.googleapis.com/v1/${extension}`;
+// }
-/**
- * This namespace encompasses the logic
- * necessary to upload images to Google's server,
- * and then initialize / create those images in the Photos
- * API given the upload tokens returned from the initial
- * uploading process.
- *
- * https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate
- */
-export namespace Uploader {
+// /**
+// * Factors out the creation of the API request's
+// * authentication elements stored in the header.
+// * @param type the contents of the request
+// * @param token the user-specific Google access token
+// */
+// function headers(type: string, token: string) {
+// return {
+// 'Content-Type': `application/${type}`,
+// Authorization: `Bearer ${token}`,
+// };
+// }
- /**
- * Specifies the structure of the object
- * necessary to upload bytes to Google's servers.
- * The url is streamed to access the image's bytes,
- * and the description is what appears in Google Photos'
- * description field.
- */
- export interface UploadSource {
- url: string;
- description: string;
- }
+// /**
+// * This is the first step in the remote image creation process.
+// * Here we upload the raw bytes of the image to Google's servers by
+// * setting authentication and other required header properties and including
+// * the raw bytes to the image, to be uploaded, in the body of the request.
+// * @param bearerToken the user-specific Google access token, specifies the account associated
+// * with the eventual image creation
+// * @param url the url of the image to upload
+// * @param filename an optional name associated with the uploaded image - if not specified
+// * defaults to the filename (basename) in the url
+// */
+// export const SendBytes = async (bearerToken: string, url: string, filename?: string): Promise<any> => {
+// // check if the url points to a non-image or an unsupported format
+// if (!DashUploadUtils.validateExtension(url)) {
+// return undefined;
+// }
+// const body = await request(url, { encoding: null }); // returns a readable stream with the unencoded binary image data
+// const parameters = {
+// method: 'POST',
+// uri: prepend('uploads'),
+// headers: {
+// ...headers('octet-stream', bearerToken),
+// 'X-Goog-Upload-File-Name': filename || path.basename(url),
+// 'X-Goog-Upload-Protocol': 'raw',
+// },
+// body,
+// };
+// return new Promise((resolve, reject) =>
+// request(parameters, (error, _response, body) => {
+// if (error) {
+// // on rejection, the server logs the error and the offending image
+// return reject(error);
+// }
+// resolve(body);
+// })
+// );
+// };
- /**
- * This is the format needed to pass
- * into the BatchCreate API request
- * to take a reference to raw uploaded bytes
- * and actually create an image in Google Photos.
- *
- * So, to instantiate this interface you must have already dispatched an upload
- * and received an upload token.
- */
- export interface NewMediaItem {
- description: string;
- simpleMediaItem: {
- uploadToken: string;
- };
- }
-
- /**
- * A utility function to streamline making
- * calls to the API's url - accentuates
- * the relative path in the caller.
- * @param extension the desired
- * subset of the API
- */
- function prepend(extension: string): string {
- return `https://photoslibrary.googleapis.com/v1/${extension}`;
- }
-
- /**
- * Factors out the creation of the API request's
- * authentication elements stored in the header.
- * @param type the contents of the request
- * @param token the user-specific Google access token
- */
- function headers(type: string, token: string) {
- return {
- 'Content-Type': `application/${type}`,
- 'Authorization': `Bearer ${token}`,
- };
- }
-
- /**
- * This is the first step in the remote image creation process.
- * Here we upload the raw bytes of the image to Google's servers by
- * setting authentication and other required header properties and including
- * the raw bytes to the image, to be uploaded, in the body of the request.
- * @param bearerToken the user-specific Google access token, specifies the account associated
- * with the eventual image creation
- * @param url the url of the image to upload
- * @param filename an optional name associated with the uploaded image - if not specified
- * defaults to the filename (basename) in the url
- */
- export const SendBytes = async (bearerToken: string, url: string, filename?: string): Promise<any> => {
- // check if the url points to a non-image or an unsupported format
- if (!DashUploadUtils.validateExtension(url)) {
- return undefined;
- }
- const body = await request(url, { encoding: null }); // returns a readable stream with the unencoded binary image data
- const parameters = {
- method: 'POST',
- uri: prepend('uploads'),
- headers: {
- ...headers('octet-stream', bearerToken),
- 'X-Goog-Upload-File-Name': filename || path.basename(url),
- 'X-Goog-Upload-Protocol': 'raw'
- },
- body
- };
- return new Promise((resolve, reject) => request(parameters, (error, _response, body) => {
- if (error) {
- // on rejection, the server logs the error and the offending image
- return reject(error);
- }
- resolve(body);
- }));
- };
-
- /**
- * This is the second step in the remote image creation process: having uploaded
- * the raw bytes of the image and received / stored pointers (upload tokens) to those
- * bytes, we can now instruct the API to finalize the creation of those images by
- * submitting a batch create request with the list of upload tokens and the description
- * to be associated with reach resulting new image.
- * @param bearerToken the user-specific Google access token, specifies the account associated
- * with the eventual image creation
- * @param newMediaItems a list of objects containing a description and, effectively, the
- * pointer to the uploaded bytes
- * @param album if included, will add all of the newly created remote images to the album
- * with the specified id
- */
- export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
- // it's important to note that the API can't handle more than 50 items in each request and
- // seems to need at least some latency between requests (spamming it synchronously has led to the server returning errors)...
- const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
- // ...so we execute them in delayed batches and await the entire execution
- return batched.batchedMapPatientInterval(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch: NewMediaItem[], collector): Promise<void> => {
- const parameters = {
- method: 'POST',
- headers: headers('json', bearerToken),
- uri: prepend('mediaItems:batchCreate'),
- body: { newMediaItems: batch } as any,
- json: true
- };
- // register the target album, if provided
- album && (parameters.body.albumId = album.id);
- collector.push(...(await new Promise<NewMediaItemResult[]>((resolve, reject) => {
- request(parameters, (error, _response, body) => {
- if (error) {
- reject(error);
- } else {
- resolve(body.newMediaItemResults);
- }
- });
- })));
- }
- );
- };
-
-} \ No newline at end of file
+// /**
+// * This is the second step in the remote image creation process: having uploaded
+// * the raw bytes of the image and received / stored pointers (upload tokens) to those
+// * bytes, we can now instruct the API to finalize the creation of those images by
+// * submitting a batch create request with the list of upload tokens and the description
+// * to be associated with reach resulting new image.
+// * @param bearerToken the user-specific Google access token, specifies the account associated
+// * with the eventual image creation
+// * @param newMediaItems a list of objects containing a description and, effectively, the
+// * pointer to the uploaded bytes
+// * @param album if included, will add all of the newly created remote images to the album
+// * with the specified id
+// */
+// export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
+// // it's important to note that the API can't handle more than 50 items in each request and
+// // seems to need at least some latency between requests (spamming it synchronously has led to the server returning errors)...
+// const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
+// // ...so we execute them in delayed batches and await the entire execution
+// return batched.batchedMapPatientInterval({ magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: NewMediaItem[], collector): Promise<void> => {
+// const parameters = {
+// method: 'POST',
+// headers: headers('json', bearerToken),
+// uri: prepend('mediaItems:batchCreate'),
+// body: { newMediaItems: batch } as any,
+// json: true,
+// };
+// // register the target album, if provided
+// album && (parameters.body.albumId = album.id);
+// collector.push(
+// ...(await new Promise<NewMediaItemResult[]>((resolve, reject) => {
+// request(parameters, (error, _response, body) => {
+// if (error) {
+// reject(error);
+// } else {
+// resolve(body.newMediaItemResults);
+// }
+// });
+// }))
+// );
+// });
+// };
+// }
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
deleted file mode 100644
index e419d3ac4..000000000
--- a/src/server/ApiManagers/PDFManager.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method } from "../RouteManager";
-import RouteSubscriber from "../RouteSubscriber";
-import { existsSync, createReadStream, createWriteStream } from "fs";
-import * as Pdfjs from 'pdfjs-dist/legacy/build/pdf';
-import { createCanvas } from "canvas";
-const imageSize = require("probe-image-size");
-import * as express from "express";
-import * as path from "path";
-import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from "./UploadManager";
-import { red } from "colors";
-import { resolve } from "path";
-
-export default class PDFManager extends ApiManager {
-
- protected initialize(register: Registration): void {
-
- register({
- method: Method.POST,
- subscription: new RouteSubscriber("thumbnail"),
- secureHandler: async ({ req, res }) => {
- const { coreFilename, pageNum, subtree } = req.body;
- return getOrCreateThumbnail(coreFilename, pageNum, res, subtree);
- }
- });
-
- }
-
-}
-
-async function getOrCreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string): Promise<void> {
- const resolved = `${coreFilename}-${pageNum}.png`;
- return new Promise<void>(async resolve => {
- const path = serverPathToFile(Directory.pdf_thumbnails, resolved);
- if (existsSync(path)) {
- const existingThumbnail = createReadStream(path);
- const { err, viewport } = await new Promise<any>(resolve => {
- imageSize(existingThumbnail, (err: any, viewport: any) => resolve({ err, viewport }));
- });
- if (err) {
- console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${resolved}:`));
- console.log(err);
- return;
- }
- dispatchThumbnail(res, viewport, resolved);
- } else {
- await CreateThumbnail(coreFilename, pageNum, res, subtree);
- }
- resolve();
- });
-}
-
-async function CreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string) {
- const part1 = subtree ?? "";
- const filename = `${part1}${coreFilename}.pdf`;
- const sourcePath = resolve(pathToDirectory(Directory.pdfs), filename);
- const documentProxy = await Pdfjs.getDocument(sourcePath).promise;
- const factory = new NodeCanvasFactory();
- const page = await documentProxy.getPage(pageNum);
- const viewport = page.getViewport({ scale: 1, rotation: 0, dontFlip: false });
- const { canvas, context } = factory.create(viewport.width, viewport.height);
- const renderContext = {
- canvasContext: context,
- canvasFactory: factory,
- viewport
- };
- await page.render(renderContext).promise;
- const pngStream = canvas.createPNGStream();
- const resolved = `${coreFilename}-${pageNum}.png`;
- const pngFile = serverPathToFile(Directory.pdf_thumbnails, resolved);
- const out = createWriteStream(pngFile);
- pngStream.pipe(out);
- return new Promise<void>((resolve, reject) => {
- out.on("finish", () => {
- dispatchThumbnail(res, viewport, resolved);
- resolve();
- });
- out.on("error", error => {
- console.log(red(`In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`));
- console.log(error);
- reject();
- });
- });
-}
-
-function dispatchThumbnail(res: express.Response, { width, height }: Pdfjs.PageViewport, thumbnailName: string) {
- res.send({
- path: clientPathToFile(Directory.pdf_thumbnails, thumbnailName),
- width,
- height
- });
-}
-
-class NodeCanvasFactory {
-
- create = (width: number, height: number) => {
- const canvas = createCanvas(width, height);
- const context = canvas.getContext('2d');
- return {
- canvas,
- context
- };
- }
-
- reset = (canvasAndContext: any, width: number, height: number) => {
- canvasAndContext.canvas.width = width;
- canvasAndContext.canvas.height = height;
- }
-
- destroy = (canvasAndContext: any) => {
- canvasAndContext.canvas.width = 0;
- canvasAndContext.canvas.height = 0;
- canvasAndContext.canvas = null;
- canvasAndContext.context = null;
- }
-}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index ea5d8cb33..9b0457a25 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -1,7 +1,7 @@
import * as formidable from 'formidable';
import { createReadStream, createWriteStream, unlink, writeFile } from 'fs';
-import { basename, dirname, extname, normalize } from 'path';
-import * as sharp from 'sharp';
+import * as path from 'path';
+import Jimp from 'jimp';
import { filesDirectory, publicDirectory } from '..';
import { retrocycle } from '../../decycler/decycler';
import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
@@ -11,7 +11,7 @@ import RouteSubscriber from '../RouteSubscriber';
import { AcceptableMedia, Upload } from '../SharedMediaTypes';
import ApiManager, { Registration } from './ApiManager';
import { SolrManager } from './SearchManager';
-import v4 = require('uuid/v4');
+import * as uuid from 'uuid';
import { DashVersion } from '../../fields/DocSymbols';
const AdmZip = require('adm-zip');
const imageDataUri = require('image-data-uri');
@@ -29,11 +29,11 @@ export enum Directory {
}
export function serverPathToFile(directory: Directory, filename: string) {
- return normalize(`${filesDirectory}/${directory}/${filename}`);
+ return path.normalize(`${filesDirectory}/${directory}/${filename}`);
}
export function pathToDirectory(directory: Directory) {
- return normalize(`${filesDirectory}/${directory}`);
+ return path.normalize(`${filesDirectory}/${directory}`);
}
export function clientPathToFile(directory: Directory, filename: string) {
@@ -63,7 +63,7 @@ export default class UploadManager extends ApiManager {
method: Method.POST,
subscription: '/uploadFormData',
secureHandler: async ({ req, res }) => {
- const form = new formidable.IncomingForm();
+ const form = new formidable.IncomingForm({ keepExtensions: true, uploadDir: pathToDirectory(Directory.parsed_files) });
let fileguids = '';
let filesize = '';
form.on('field', (e: string, value: string) => {
@@ -74,28 +74,32 @@ export default class UploadManager extends ApiManager {
filesize = value;
}
});
+ fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `upload starting`));
+
form.on('progress', e => fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `read:(${Math.round((100 * +e) / +filesize)}%) ${e} of ${filesize}`)));
- form.keepExtensions = true;
- form.uploadDir = pathToDirectory(Directory.parsed_files);
return new Promise<void>(resolve => {
form.parse(req, async (_err, _fields, files) => {
const results: Upload.FileResponse[] = [];
if (_err?.message) {
results.push({
source: {
+ filepath: '',
+ originalFilename: 'none',
+ newFilename: 'none',
+ mimetype: 'text',
size: 0,
- path: 'none',
- name: 'none',
- type: 'none',
- toJSON: () => ({ name: 'none', path: '' }),
+ hashAlgorithm: 'md5',
+ toJSON: () => ({ name: 'none', size: 0, length: 0, mtime: new Date(), filepath: '', originalFilename: 'none', newFilename: 'none', mimetype: 'text' }),
},
result: { name: 'failed upload', message: `${_err.message}` },
});
}
+ fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `resampling images`));
+
for (const key in files) {
const f = files[key];
- if (!Array.isArray(f)) {
- const result = await DashUploadUtils.upload(f, key); // key is the guid used by the client to track upload progress.
+ if (f) {
+ const result = await DashUploadUtils.upload(f[0], key); // key is the guid used by the client to track upload progress.
result && !(result.result instanceof Error) && results.push(result);
}
}
@@ -164,7 +168,7 @@ export default class UploadManager extends ApiManager {
if (error) {
return res.send();
}
- await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
+ await DashUploadUtils.outputResizedImages(resolvedPath, resolvedName, pathToDirectory(Directory.images));
res.send({
accessPaths: {
agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName),
@@ -193,15 +197,14 @@ export default class UploadManager extends ApiManager {
method: Method.POST,
subscription: '/uploadDoc',
secureHandler: ({ req, res }) => {
- const form = new formidable.IncomingForm();
- form.keepExtensions = true;
+ const form = new formidable.IncomingForm({ keepExtensions: true });
// let path = req.body.path;
const ids: { [id: string]: string } = {};
let remap = true;
const getId = (id: string): string => {
if (!remap || id.endsWith('Proto')) return id;
if (id in ids) return ids[id];
- return (ids[id] = v4());
+ return (ids[id] = uuid.v4());
};
const mapFn = (doc: any) => {
if (doc.id) {
@@ -241,56 +244,35 @@ export default class UploadManager extends ApiManager {
};
return new Promise<void>(resolve => {
form.parse(req, async (_err, fields, files) => {
- remap = fields.remap !== 'false';
+ remap = Object.keys(fields).some(key => key === 'remap' && !fields.remap?.includes('false')); //.remap !== 'false'; // bcz: looking to see if the field 'remap' is set to 'false'
let id: string = '';
let docids: string[] = [];
let linkids: string[] = [];
try {
for (const name in files) {
const f = files[name];
- const path_2 = Array.isArray(f) ? '' : f.path;
- const zip = new AdmZip(path_2);
+ if (!f) continue;
+ const path_2 = f[0]; // what about the rest of the array? are we guaranteed only one value is set?
+ const zip = new AdmZip(path_2.filepath);
zip.getEntries().forEach((entry: any) => {
let entryName = entry.entryName.replace(/%%%/g, '/');
if (!entryName.startsWith('files/')) {
return;
}
- const extension = extname(entryName);
+ const extension = path.extname(entryName);
const pathname = publicDirectory + '/' + entry.entryName;
const targetname = publicDirectory + '/' + entryName;
try {
zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
createReadStream(pathname).pipe(createWriteStream(targetname));
- if (extension !== '.pdf') {
- const { pngs, jpgs } = AcceptableMedia;
- const resizers = [
- { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Small },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Large },
- ];
- let isImage = false;
- if (pngs.includes(extension)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.png();
- });
- isImage = true;
- } else if (jpgs.includes(extension)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.jpeg();
- });
- isImage = true;
- }
- if (isImage) {
- resizers.forEach(resizer => {
- createReadStream(pathname)
- .on('error', e => console.log('Resizing read:' + e))
- .pipe(resizer.resizer)
- .on('error', e => console.log('Resizing write: ' + e))
- .pipe(createWriteStream(targetname.replace('_o' + extension, resizer.suffix + extension)).on('error', e => console.log('Resizing write: ' + e)));
- });
- }
- }
- unlink(pathname, () => {});
+ Jimp.read(pathname).then(img => {
+ DashUploadUtils.imageResampleSizes(extension).forEach(({ width, suffix }) => {
+ const outputPath = InjectSize(targetname, suffix);
+ if (!width) createReadStream(pathname).pipe(createWriteStream(outputPath));
+ else img = img.resize(width, Jimp.AUTO).write(outputPath);
+ });
+ unlink(pathname, () => {});
+ });
} catch (e) {
console.log(e);
}
@@ -317,7 +299,7 @@ export default class UploadManager extends ApiManager {
} catch (e) {
console.log(e);
}
- unlink(path_2, () => {});
+ unlink(path_2.filepath, () => {});
}
SolrManager.update();
res.send(JSON.stringify({ id, docids, linkids } || 'error'));
@@ -346,7 +328,7 @@ export default class UploadManager extends ApiManager {
method: Method.POST,
subscription: '/uploadURI',
secureHandler: ({ req, res }) => {
- const uri = req.body.uri;
+ const uri: any = req.body.uri;
const filename = req.body.name;
const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
const deleteFiles = req.body.replaceRootFilename;
@@ -362,36 +344,16 @@ export default class UploadManager extends ApiManager {
.map((f: any) => fs.unlinkSync(path + f));
}
return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
- const ext = extname(savedName).toLowerCase();
- const { pngs, jpgs } = AcceptableMedia;
- const resizers = !origSuffix
- ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }]
- : [
- { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Small },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Large },
- ];
- let isImage = false;
- if (pngs.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.png();
- });
- isImage = true;
- } else if (jpgs.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.jpeg();
- });
- isImage = true;
- }
- if (isImage) {
- resizers.forEach(resizer => {
- const path = serverPathToFile(Directory.images, InjectSize(filename, resizer.suffix) + ext);
- createReadStream(savedName)
- .on('error', e => console.log('Resizing read:' + e))
- .pipe(resizer.resizer)
- .on('error', e => console.log('Resizing write: ' + e))
- .pipe(createWriteStream(path).on('error', e => console.log('Resizing write: ' + e)));
- });
+ const ext = path.extname(savedName).toLowerCase();
+ if (AcceptableMedia.imageFormats.includes(ext)) {
+ Jimp.read(savedName).then(img =>
+ (!origSuffix ? [{ width: 400, suffix: SizeSuffix.Medium }] : Object.values(DashUploadUtils.Sizes)) //
+ .forEach(({ width, suffix }) => {
+ const outputPath = serverPathToFile(Directory.images, InjectSize(filename, suffix) + ext);
+ if (!width) createReadStream(savedName).pipe(createWriteStream(outputPath));
+ else img = img.resize(width, Jimp.AUTO).write(outputPath);
+ })
+ );
}
res.send(clientPathToFile(Directory.images, filename + ext));
});
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index 8b7994eac..0431b9bcf 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -7,6 +7,8 @@ import { Opt } from '../../fields/Doc';
import { WebSocket } from '../websocket';
import { resolvedPorts } from '../server_Initialization';
import { DashVersion } from '../../fields/DocSymbols';
+import { Utils } from '../../Utils';
+import { check, validationResult } from 'express-validator';
export const timeMap: { [id: string]: number } = {};
interface ActivityUnit {
@@ -32,7 +34,7 @@ export default class UserManager extends ApiManager {
secureHandler: async ({ user, req, res }) => {
const result: any = {};
user.cacheDocumentIds = req.body.cacheDocumentIds;
- user.save(err => {
+ user.save().then(undefined, err => {
if (err) {
result.error = [{ msg: 'Error while caching documents' }];
}
@@ -49,7 +51,7 @@ export default class UserManager extends ApiManager {
method: Method.GET,
subscription: '/getUserDocumentIds',
secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }),
- publicHandler: ({ res }) => res.send({ userDocumentId: '__guest__', linkDatabaseId: 3, sharingDocumentId: 2 }),
+ publicHandler: ({ res }) => res.send({ userDocumentId: Utils.GuestID(), linkDatabaseId: 3, sharingDocumentId: 2 }),
});
register({
@@ -81,7 +83,7 @@ export default class UserManager extends ApiManager {
resolvedPorts,
})
),
- publicHandler: ({ res }) => res.send(JSON.stringify({ id: '__guest__', email: 'guest' })),
+ publicHandler: ({ res }) => res.send(JSON.stringify({ userDocumentId: Utils.GuestID(), email: 'guest', resolvedPorts })),
});
register({
@@ -107,15 +109,18 @@ export default class UserManager extends ApiManager {
return;
}
- req.assert('new_pass', 'Password must be at least 4 characters long').len({ min: 4 });
- req.assert('new_confirm', 'Passwords do not match').equals(new_pass);
+ check('new_pass', 'Password must be at least 4 characters long')
+ .run(req)
+ .then(chcekcres => console.log(chcekcres)); //.len({ min: 4 });
+ check('new_confirm', 'Passwords do not match')
+ .run(req)
+ .then(theres => console.log(theres)); //.equals(new_pass);
if (curr_pass === new_pass) {
result.error = [{ msg: 'Current and new password are the same' }];
}
- // was there error in validating new passwords?
- if (req.validationErrors()) {
- // was there error?
- result.error = req.validationErrors();
+ if (validationResult(req).array().length) {
+ // was there error in validating new passwords?
+ result.error = validationResult(req);
}
// will only change password if there are no errors.
@@ -125,7 +130,7 @@ export default class UserManager extends ApiManager {
user.passwordResetExpires = undefined;
}
- user.save(err => {
+ user.save().then(undefined, err => {
if (err) {
result.error = [{ msg: 'Error while saving new password' }];
}