diff options
Diffstat (limited to 'src/server/ApiManagers')
| -rw-r--r-- | src/server/ApiManagers/DeleteManager.ts | 36 | ||||
| -rw-r--r-- | src/server/ApiManagers/GooglePhotosManager.ts | 621 | ||||
| -rw-r--r-- | src/server/ApiManagers/PDFManager.ts | 116 | ||||
| -rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 130 | ||||
| -rw-r--r-- | src/server/ApiManagers/UserManager.ts | 25 |
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' }]; } |
