aboutsummaryrefslogtreecommitdiff
path: root/src/server/ApiManagers/GooglePhotosManager.ts
blob: e2539f120d5eec7730c4aad37ee86f024639e7b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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 { GooglePhotosUploadUtils } from "../apis/google/GooglePhotosUploadUtils";
import { Opt } from "../../new_fields/Doc";
import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
import { Database } from "../database";

const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
const mediaError = "Unable to convert all uploaded bytes to media items!";
const UploadError = (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 MediaItem {
    baseUrl: string;
    filename: string;
}
interface NewMediaItem {
    description: string;
    simpleMediaItem: {
        uploadToken: string;
    };
}
const prefix = "google_photos_";

/**
 * This manager handles the creation of routes for google photos functionality.
 */
export default class GooglePhotosManager extends ApiManager {

    protected initialize(register: Registration): void {

        register({
            method: Method.POST,
            subscription: "/googlePhotosMediaUpload",
            onValidation: async ({ user, req, res }) => {
                const { media } = req.body;
                const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
                if (!token) {
                    return _error(res, authenticationError);
                }
                const failed: GooglePhotosUploadFailure[] = [];
                const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
                const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
                    { magnitude: 100, unit: TimeUnit.Milliseconds },
                    async (batch: any, collector: any, { completedBatches }: any) => {
                        for (let index = 0; index < batch.length; index++) {
                            const { url, description } = batch[index];
                            const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
                            const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, InjectSize(url, SizeSuffix.Original)).catch(fail);
                            if (!uploadToken) {
                                fail(`${path.extname(url)} is not an accepted extension`);
                            } else {
                                collector.push({
                                    description,
                                    simpleMediaItem: { uploadToken }
                                });
                            }
                        }
                    }
                );
                const failedCount = failed.length;
                if (failedCount) {
                    console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
                    console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
                }
                return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
                    results => _success(res, { results, failed }),
                    error => _error(res, mediaError, error)
                );
            }
        });

        register({
            method: Method.POST,
            subscription: "/googlePhotosMediaDownload",
            onValidation: async ({ req, res }) => {
                const contents: { mediaItems: MediaItem[] } = req.body;
                let failed = 0;
                if (contents) {
                    const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
                    for (const item of contents.mediaItems) {
                        const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
                        const found: Opt<DashUploadUtils.ImageUploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
                        if (!found) {
                            const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
                            if (upload) {
                                completed.push(upload);
                                await Database.Auxiliary.LogUpload(upload);
                            } else {
                                failed++;
                            }
                        } else {
                            completed.push(found);
                        }
                    }
                    if (failed) {
                        return _error(res, UploadError(failed));
                    }
                    return _success(res, completed);
                }
                _invalid(res, requestError);
            }
        });

    }
}