aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts19
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx6
-rw-r--r--src/new_fields/RichTextUtils.ts115
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts56
-rw-r--r--src/server/apis/google/existing_uploads.json1
-rw-r--r--src/server/credentials/google_docs_token.json2
-rw-r--r--src/server/index.ts30
7 files changed, 162 insertions, 67 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 828d4451a..cbc5da15b 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -97,25 +97,30 @@ export namespace GoogleApiClientUtils {
export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
let paragraphs = extractParagraphs(document);
- let text = paragraphs.map(paragraph => paragraph.runs.map(run => run.content).join("")).join("");
+ let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => run as docs_v1.Schema$TextRun).join("")).join("");
text = text.substring(0, text.length - 1);
removeNewlines && text.ReplaceAll("\n", "");
return { text, paragraphs };
};
- export type DeconstructedParagraph = { runs: docs_v1.Schema$TextRun[], bullet: Opt<number> };
+ export type ContentArray = (docs_v1.Schema$TextRun | docs_v1.Schema$InlineObjectElement)[];
+ export type DeconstructedParagraph = { contents: ContentArray, bullet: Opt<number> };
const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
const fragments: DeconstructedParagraph[] = [];
if (document.body && document.body.content) {
for (const element of document.body.content) {
- let runs: docs_v1.Schema$TextRun[] = [];
+ let runs: ContentArray = [];
let bullet: Opt<number>;
if (element.paragraph) {
if (element.paragraph.elements) {
for (const inner of element.paragraph.elements) {
- if (inner && inner.textRun) {
- let run = inner.textRun;
- (run.content || !filterEmpty) && runs.push(inner.textRun);
+ if (inner) {
+ if (inner.textRun) {
+ let run = inner.textRun;
+ (run.content || !filterEmpty) && runs.push(inner.textRun);
+ } else if (inner.inlineObjectElement) {
+ runs.push(inner.inlineObjectElement);
+ }
}
}
}
@@ -123,7 +128,7 @@ export namespace GoogleApiClientUtils {
bullet = element.paragraph.bullet.nestingLevel || 0;
}
}
- (runs.length || !filterEmpty) && fragments.push({ runs, bullet });
+ (runs.length || !filterEmpty) && fragments.push({ contents: runs, bullet });
}
}
return fragments;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index fc5b27220..8f0f142c4 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -193,8 +193,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id);
});
});
- const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
- const mval = this._editorView!.state.schema.marks.metadataVal.create();
+ const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
+ const mval = this._editorView.state.schema.marks.metadataVal.create();
let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
this.dataDoc[key] = value;
@@ -506,7 +506,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let documentId = StrCast(dataDoc[GoogleRef]);
let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
if (documentId) {
- exportState = await RichTextUtils.GoogleDocs.Import(documentId);
+ exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
}
UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
}
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 555c41b67..ab5e677c8 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -14,9 +14,10 @@ import { schema } from "../client/util/RichTextSchema";
import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
import { SchemaHeaderField } from "./SchemaHeaderField";
import { DocServer } from "../client/DocServer";
-import { Cast } from "./Types";
+import { Cast, StrCast } from "./Types";
import { Id } from "./FieldSymbols";
import { DocumentView } from "../client/views/nodes/DocumentView";
+import { AssertionError } from "assert";
export namespace RichTextUtils {
@@ -109,45 +110,78 @@ export namespace RichTextUtils {
return { text, requests };
};
+ interface ImageTemplate {
+ width: number;
+ title: string;
+ url: string;
+ }
+
+ const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise<Map<string, ImageTemplate>> => {
+ const inlineObjectMap = new Map<string, ImageTemplate>();
+ const inlineObjects = document.inlineObjects;
+
+ if (inlineObjects) {
+ const objects = Object.keys(inlineObjects).map(objectId => inlineObjects[objectId]);
+ const mediaItems: MediaItem[] = objects.map(object => {
+ const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
+ const baseUrl = embeddedObject.imageProperties!.contentUri!;
+ const filename = `upload_${Utils.GenerateGuid()}.png`;
+ return { baseUrl, filename };
+ });
+
+ const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems });
+
+ if (uploads.length !== mediaItems.length) {
+ throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
+ }
+
+ for (let i = 0; i < objects.length; i++) {
+ const object = objects[i];
+ const { fileNames } = uploads[i];
+ const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
+ const size = embeddedObject.size!;
+ const width = size.width!.magnitude!;
+ const url = Utils.fileUrl(fileNames.clean);
+
+ inlineObjectMap.set(object.objectId!, {
+ title: embeddedObject.title || `Imported Image from ${document.title}`,
+ width,
+ url
+ });
+ }
+ }
+ return inlineObjectMap;
+ };
+
type BulletPosition = { value: number, sinks: number };
interface MediaItem {
baseUrl: string;
filename: string;
- width: number;
}
- export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
+
+ export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId, textNote: Doc): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
const document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
if (!document) {
return undefined;
}
-
+ const inlineObjectMap = await parseInlineObjects(document);
const title = document.title!;
const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
let state = FormattedTextBox.blankState();
let structured = parseLists(paragraphs);
- const inline = document.inlineObjects;
- let inlineUrls: MediaItem[] = [];
- if (inline) {
- inlineUrls = Object.keys(inline).map(key => {
- const embedded = inline[key].inlineObjectProperties!.embeddedObject!;
- const baseUrl = embedded.imageProperties!.contentUri!;
- const filename = `upload_${Utils.GenerateGuid()}.png`;
- const width = embedded.size!.width!.magnitude!;
- return { baseUrl, filename, width };
- });
- }
let position = 3;
let lists: ListGroup[] = [];
const indentMap = new Map<ListGroup, BulletPosition[]>();
let globalOffset = 0;
- const nodes = structured.map(element => {
+ const nodes: Node<any>[] = [];
+ for (let element of structured) {
if (Array.isArray(element)) {
lists.push(element);
let positions: BulletPosition[] = [];
let items = element.map(paragraph => {
- let item = listItem(state.schema, paragraph.runs);
+ let item = listItem(state.schema, paragraph.contents);
let sinks = paragraph.bullet!;
positions.push({
value: position + globalOffset,
@@ -158,13 +192,26 @@ export namespace RichTextUtils {
return item;
});
indentMap.set(element, positions);
- return list(state.schema, items);
+ nodes.push(list(state.schema, items));
} else {
- let paragraph = paragraphNode(state.schema, element.runs);
- position += paragraph.nodeSize;
- return paragraph;
+ if (element.contents.some(child => "inlineObjectId" in child)) {
+ let node: Node<any>;
+ for (const child of element.contents) {
+ if ("inlineObjectId" in child) {
+ node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote);
+ } else {
+ node = paragraphNode(state.schema, [child]);
+ }
+ nodes.push(node);
+ position += node.nodeSize;
+ }
+ } else {
+ let paragraph = paragraphNode(state.schema, element.contents);
+ nodes.push(paragraph);
+ position += paragraph.nodeSize;
+ }
}
- });
+ }
state = state.apply(state.tr.replaceWith(0, 2, nodes));
let sink = sinkListItem(state.schema.nodes.list_item);
@@ -179,14 +226,6 @@ export namespace RichTextUtils {
}
}
- const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems: inlineUrls });
- for (let i = 0; i < uploads.length; i++) {
- const src = Utils.fileUrl(uploads[i].fileNames.clean);
- const width = inlineUrls[i].width;
- const imageNode = schema.nodes.image.create({ src, width });
- state = state.apply(state.tr.insert(0, imageNode));
- }
-
return { title, text, state };
};
@@ -226,6 +265,22 @@ export namespace RichTextUtils {
return schema.node("paragraph", null, fragment);
};
+ const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
+ const { url: src, width } = image;
+ let docid: string;
+ const guid = Utils.GenerateDeterministicGuid(src);
+ const backingDocId = StrCast(textNote[guid]);
+ if (!backingDocId) {
+ const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 });
+ DocumentView.makeCustomViewClicked(backingDoc);
+ docid = backingDoc[Id];
+ textNote[guid] = docid;
+ } else {
+ docid = backingDocId;
+ }
+ return schema.node("image", { src, width, docid });
+ };
+
const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
let text = run.content!.removeTrailingNewlines();
return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined;
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index f582cebd2..3ab9ba90f 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -118,28 +118,44 @@ export namespace DownloadUtils {
const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
- export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => {
- const resolved = filename ? sanitize(filename) : generate(prefix, url);
- let extension = path.extname(url) || path.extname(resolved);
+ export interface InspectionResults {
+ isLocal: boolean;
+ stream: any;
+ normalizedUrl: string;
+ contentSize: number;
+ contentType: string;
+ }
+
+ export const InspectImage = async (url: string) => {
+ const { isLocal, stream, normalized: normalizedUrl } = classify(url);
+ const metadata = (await new Promise<any>((resolve, reject) => {
+ request.head(url, async (error, res) => {
+ if (error) {
+ return reject(error);
+ }
+ resolve(res);
+ });
+ })).headers;
+ return {
+ contentSize: parseInt(metadata[size]),
+ contentType: metadata[type],
+ isLocal,
+ stream,
+ normalizedUrl
+ };
+ };
+
+ export const UploadImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => {
+ const { isLocal, stream, normalizedUrl, contentSize, contentType } = metadata;
+ const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
+ let extension = path.extname(normalizedUrl) || path.extname(resolved);
extension && (extension = extension.toLowerCase());
let information: UploadInformation = {
mediaPaths: [],
- fileNames: { clean: resolved }
+ fileNames: { clean: resolved },
+ contentSize,
+ contentType,
};
- const { isLocal, stream, normalized } = classify(url);
- url = normalized;
- if (!isLocal) {
- const metadata = (await new Promise<any>((resolve, reject) => {
- request.head(url, async (error, res) => {
- if (error) {
- return reject(error);
- }
- resolve(res);
- });
- })).headers;
- information.contentSize = parseInt(metadata[size]);
- information.contentType = metadata[type];
- }
return new Promise<UploadInformation>(async (resolve, reject) => {
const resizers = [
{ resizer: sharp().rotate(), suffix: "_o" },
@@ -164,7 +180,7 @@ export namespace DownloadUtils {
const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
information.mediaPaths.push(mediaPath = uploadDirectory + filename);
information.fileNames[suffix] = filename;
- stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
+ stream(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
.on('close', resolve)
.on('error', reject);
});
@@ -172,7 +188,7 @@ export namespace DownloadUtils {
}
if (!isLocal || nonVisual) {
await new Promise<void>(resolve => {
- stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+ stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
});
}
resolve(information);
diff --git a/src/server/apis/google/existing_uploads.json b/src/server/apis/google/existing_uploads.json
new file mode 100644
index 000000000..05c20c33b
--- /dev/null
+++ b/src/server/apis/google/existing_uploads.json
@@ -0,0 +1 @@
+{"23625":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_l.png"],"fileNames":{"clean":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180.png","_o":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_o.png","_s":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_s.png","_m":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_m.png","_l":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_l.png"},"contentSize":23625,"contentType":"image/jpeg"}} \ 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 4f2fb0f9d..c10b0797f 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GlyEB-6kaRm7dCD9x3j1b5AyujXvfpS5NWuJQwy6UKLO06KYXcF2e5XaCxvR7QJgH3Pn2iu3btjYrrJxNNaLffgEszcJHNsN_5IIWJBA4sdG6KLW63MmFwfV4U1hyQ","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":1568573667294} \ No newline at end of file
+{"access_token":"ya29.ImCFB_ghOybVB6A4HvIIwIlyGyZw6wOymdwJyWJJECIpCmFTHNEzOAfP98KFzm5OUV2zZNS5Wx1iUT1xYWW35PY7NoZc7PWwjzmOaGkMzDm7_fxpsgjT0StdvEwTJprFIv0","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":1568590984976} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 2e60d9be7..07ce4b6f0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -46,6 +46,7 @@ const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const probe = require("probe-image-size");
import * as qs from 'query-string';
+import { Opt } from '../new_fields/Doc';
const extensions = require("../client/util/UtilExtensions");
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
@@ -580,7 +581,8 @@ app.post(
for (const key in files) {
const { type, path: location, name } = files[key];
const filename = path.basename(location);
- await UploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`));
+ const metadata = await UploadUtils.InspectImage(uploadDirectory + filename);
+ await UploadUtils.UploadImage(metadata, filename).catch(() => console.log(`Unable to process ${filename}`));
results.push({ name, type, path: `/files/${filename}` });
}
_success(res, results);
@@ -884,14 +886,30 @@ const prefix = "google_photos_";
const downloadError = "Encountered an error while executing downloads.";
const requestError = "Unable to execute download: the body's media items were malformed.";
+app.get("/gapiCleanup", (req, res) => {
+ write_text_file(file, "");
+ res.redirect(RouteStore.delete);
+});
+
+const file = "./apis/google/existing_uploads.json";
app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
const contents: { mediaItems: MediaItem[] } = req.body;
if (contents) {
- const pending = contents.mediaItems.map(item =>
- UploadUtils.UploadImage(item.baseUrl, item.filename, prefix)
- );
- const completed = await Promise.all(pending).catch(error => _error(res, downloadError, error));
- Array.isArray(completed) && _success(res, completed);
+ const completed: Opt<UploadUtils.UploadInformation>[] = [];
+ const content = await read_text_file(file);
+ let existing = content.length ? JSON.parse(content) : {};
+ for (let item of contents.mediaItems) {
+ const { contentSize, ...attributes } = await UploadUtils.InspectImage(item.baseUrl);
+ const found: UploadUtils.UploadInformation = existing[contentSize];
+ if (!found) {
+ const upload = await UploadUtils.UploadImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
+ upload && completed.push(existing[contentSize] = upload);
+ } else {
+ completed.push(found);
+ }
+ }
+ await write_text_file(file, JSON.stringify(existing));
+ _success(res, completed);
return;
}
_invalid(res, requestError);