diff options
author | Sam Wilkins <samwilkins333@gmail.com> | 2019-09-08 04:16:04 -0400 |
---|---|---|
committer | Sam Wilkins <samwilkins333@gmail.com> | 2019-09-08 04:16:04 -0400 |
commit | f18e2265e5d468f1cbf6e82dd5f01d5f5216b851 (patch) | |
tree | b87db39dc7fa524100f712fbb87429c8d04abed3 | |
parent | c24f5f29dff8dd22f1d4029a2722ee4d1a725aad (diff) |
factored out collection creation, sharp() resizers and image management
-rw-r--r-- | src/client/apis/google_docs/GooglePhotosClientUtils.ts | 27 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
-rw-r--r-- | src/server/apis/google/GooglePhotosUploadUtils.ts | 119 | ||||
-rw-r--r-- | src/server/credentials/google_docs_token.json | 2 | ||||
-rw-r--r-- | src/server/index.ts | 27 |
6 files changed, 131 insertions, 55 deletions
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index bb5d23971..5f5b39b14 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,16 +1,16 @@ import { PostToServer, Utils } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { ImageField } from "../../../new_fields/URLField"; -import { StrCast, Cast } from "../../../new_fields/Types"; +import { Cast } from "../../../new_fields/Types"; import { Doc, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import requestImageSize = require('../../util/request-image-size'); import Photos = require('googlephotos'); import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from "../../../new_fields/RichTextUtils"; import { EditorState } from "prosemirror-state"; import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; -import { Docs } from "../../documents/Documents"; +import { Docs, DocumentOptions } from "../../documents/Documents"; +import { type } from "os"; export namespace GooglePhotosClientUtils { @@ -98,7 +98,7 @@ export namespace GooglePhotosClientUtils { excluded: [], date: undefined, includeArchivedMedia: true, - type: MediaType.ALL_MEDIA + type: MediaType.ALL_MEDIA, }; export interface SearchResponse { @@ -106,7 +106,18 @@ export namespace GooglePhotosClientUtils { nextPageToken: string; } - export const Search = async (requested: Opt<Partial<SearchOptions>>) => { + export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc; + export const CollectionFromSearch = async (provider: CollectionConstructor, requested: Opt<Partial<SearchOptions>>): Promise<Doc> => { + let downloads = await Search(requested); + return provider(downloads.map((download: any) => { + let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileNames.clean}`)); + document.fillColumn = true; + document.contentSize = download.contentSize; + return document; + }), { width: 500, height: 500 }); + }; + + export const Search = async (requested: Opt<Partial<SearchOptions>>): Promise<any> => { const options = requested || DefaultSearchOptions; const photos = await endpoint(); const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); @@ -133,11 +144,7 @@ export namespace GooglePhotosClientUtils { return new Promise<Doc>(resolve => { photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => { - response && resolve(Docs.Create.StackingDocument((await PostToServer(RouteStore.googlePhotosMediaDownload, response)).map((download: any) => { - let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileName}`)); - document.contentSize = download.contentSize; - return document; - }), { width: 500, height: 500 })); + response && resolve(await PostToServer(RouteStore.googlePhotosMediaDownload, response)); }); }); }; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4b7f1eeb6..9bac57d16 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -21,7 +21,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; -import { OmitKeys, JSONUtils } from "../../Utils"; +import { OmitKeys, JSONUtils, Utils } from "../../Utils"; import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; @@ -332,7 +332,12 @@ export namespace Docs { export function ImageDocument(url: string, options: DocumentOptions = {}) { let imgField = new ImageField(new URL(url)); let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options }); - requestImageSize(imgField.url.href) + let target = imgField.url.href; + if (new RegExp(window.location.origin).test(target)) { + let extension = path.extname(target); + target = `${target.substring(0, target.length - extension.length)}_o${extension}`; + } + requestImageSize(target) .then((size: any) => { let aspect = size.height / size.width; if (!inst.proto!.nativeWidth) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b72df3715..326c13424 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -470,7 +470,7 @@ export class MainView extends React.Component { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); - let googlePhotosSearch = () => GooglePhotosClientUtils.Search({ included: [GooglePhotosClientUtils.ContentCategories.ANIMALS] }); + let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 9b3e68761..5ac3eaef7 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -4,6 +4,9 @@ import * as fs from 'fs'; import { Utils } from '../../../Utils'; import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; +import * as sharp from 'sharp'; + +const uploadDirectory = path.join(__dirname, "../../public/files/"); export namespace GooglePhotosUploadUtils { @@ -18,13 +21,6 @@ export namespace GooglePhotosUploadUtils { description: string; } - export interface DownloadInformation { - mediaPath: string; - fileName: string; - contentType?: string; - contentSize?: string; - } - const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`; const headers = (type: string) => ({ 'Content-Type': `application/${type}`, @@ -76,35 +72,92 @@ export namespace GooglePhotosUploadUtils { }); }; - export namespace IOUtils { +} - export const Download = async (url: string, filename?: string, prefix = ""): Promise<Opt<DownloadInformation>> => { - const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; - const mediaPath = Paths.uploadDirectory + resolved; - return new Promise<DownloadInformation>((resolve, reject) => { - request.head(url, (error, res) => { - if (error) { - return reject(error); - } - const information: DownloadInformation = { - fileName: resolved, - contentSize: res.headers['content-length'], - contentType: res.headers['content-type'], - mediaPath - }; - request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information)); - }); - }); - }; +export namespace DownloadUtils { - export const createIfNotExists = async (path: string) => { - if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) { - return true; - } - return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null))); - }; + export interface Size { + width: number; + suffix: string; + } - export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null))); + export const Sizes: { [size: string]: Size } = { + SMALL: { width: 100, suffix: "_s" }, + MEDIUM: { width: 400, suffix: "_m" }, + LARGE: { width: 900, suffix: "_l" }, + }; + + const png = ".png"; + const pngs = [".png", ".PNG"]; + const jpg = [".jpg", ".JPG", ".jpeg", ".JPEG"]; + const size = "content-length"; + const type = "content-type"; + + export interface DownloadInformation { + mediaPaths: string[]; + fileNames: { [key: string]: string }; + contentSize?: string; + contentType?: string; } + const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + const sanitize = (filename: string) => filename.replace(/\s+/g, "_"); + + export const Download = async (url: string, filename?: string, prefix = ""): Promise<Opt<DownloadInformation>> => { + const resolved = filename ? sanitize(filename) : generate(prefix, url); + const extension = path.extname(url) || path.extname(resolved) || png; + return new Promise<DownloadInformation>((resolve, reject) => { + request.head(url, async (error, res) => { + if (error) { + return reject(error); + } + const information: DownloadInformation = { + fileNames: { clean: resolved }, + contentSize: res.headers[size], + contentType: res.headers[type], + mediaPaths: [] + }; + const resizers = [ + { resizer: sharp().rotate(), suffix: "_o" }, + ...Object.values(Sizes).map(size => ({ + resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(), + suffix: size.suffix + })) + ]; + let validated = true; + if (pngs.includes(extension)) { + resizers.forEach(element => element.resizer = element.resizer.png()); + } else if (jpg.includes(extension)) { + resizers.forEach(element => element.resizer = element.resizer.jpeg()); + } else { + validated = false; + } + if (validated) { + for (let resizer of resizers) { + const suffix = resizer.suffix; + let mediaPath: string; + await new Promise<void>(resolve => { + const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; + information.mediaPaths.push(mediaPath = uploadDirectory + filename); + information.fileNames[suffix] = filename; + request(url) + .pipe(resizer.resizer) + .pipe(fs.createWriteStream(mediaPath)) + .on('close', resolve); + }); + } + resolve(information); + } + }); + }); + }; + + export const createIfNotExists = async (path: string) => { + if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) { + return true; + } + return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null))); + }; + + export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null))); }
\ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index a1c23ea35..4f911f7e0 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx9B3Fumh3qHpgasQvHNNrwNXtmTVWJR9XckFsnUjOswDOO91ccF3FhD4ko7Z-3rvxEljpP1Qj5BgNq305pt-pgIquoLPWYiaEtinHNF7IXGPz4s4raqJWEJPJxow","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567913435149}
\ No newline at end of file +{"access_token":"ya29.Glx9B3_l5JbKNtvzx378Nsz917bP-OTKf6VZzc2K8QDBm-Y0j_-c8v7bL8LCEM3wF8d7JauF-5Z4Uq4v7wPwUQUlDO1uPyoHeSF6iz98xkgJr9OW4KzJo2Ij722gpQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567931641928}
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 49010e7e2..013345a76 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,7 +42,7 @@ var AdmZip = require('adm-zip'); import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; -import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils'; +import { GooglePhotosUploadUtils, DownloadUtils } from './apis/google/GooglePhotosUploadUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -154,6 +154,11 @@ app.get("/buxton", (req, res) => { command_line('python scraper.py', cwd).then(onResolved, tryPython3); }); +const STATUS = { + OK: 200, + BAD_REQUEST: 400 +}; + const command_line = (command: string, fromDirectory?: string) => { return new Promise<string>((resolve, reject) => { let options: ExecOptions = {}; @@ -848,16 +853,22 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { ); }); +interface MediaItem { + baseUrl: string; + filename: string; +} const prefix = "google_photos_"; + app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { - const contents = req.body; - if (!contents) { - return res.send(undefined); + const contents: { mediaItems: MediaItem[] } = req.body; + if (contents) { + const downloads = contents.mediaItems.map(item => + DownloadUtils.Download(item.baseUrl, item.filename, prefix) + ); + res.status(STATUS.OK).send(await Promise.all(downloads)); + return; } - await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); - res.send(await Promise.all(contents.mediaItems.map((item: any) => - GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, undefined, prefix))) - ); + res.status(STATUS.BAD_REQUEST).send(); }); const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { |