@pipedream/google_drive 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/add-file-sharing-preference/add-file-sharing-preference.mjs +83 -0
- package/actions/copy-file/copy-file.mjs +34 -0
- package/actions/create-file/create-file.mjs +242 -0
- package/actions/create-file-from-template/create-file-from-template.mjs +98 -0
- package/actions/create-file-from-text/create-file-from-text.mjs +67 -0
- package/actions/create-folder/create-folder.mjs +89 -0
- package/actions/create-shared-drive/create-shared-drive.mjs +25 -0
- package/actions/delete-file/delete-file.mjs +37 -0
- package/actions/delete-shared-drive/delete-shared-drive.mjs +30 -0
- package/actions/download-file/download-file.mjs +120 -0
- package/actions/find-file/find-file.mjs +35 -0
- package/actions/find-folder/find-folder.mjs +49 -0
- package/actions/get-folder-id-for-path/get-folder-id-for-path.mjs +62 -0
- package/actions/get-shared-drive/get-shared-drive.mjs +37 -0
- package/actions/google-mime-types.mjs +19 -0
- package/actions/google-workspace-export-formats.mjs +74 -0
- package/actions/language-codes.mjs +742 -0
- package/actions/move-file/move-file.mjs +52 -0
- package/actions/move-file-to-trash/move-file-to-trash.mjs +41 -0
- package/actions/replace-file/replace-file.mjs +134 -0
- package/actions/search-shared-drives/search-shared-drives.mjs +34 -0
- package/actions/update-file/update-file.mjs +164 -0
- package/actions/update-shared-drive/update-shared-drive.mjs +77 -0
- package/actions/upload-file/upload-file.mjs +120 -0
- package/constants.mjs +200 -0
- package/google_drive.app.mjs +1432 -0
- package/package.json +23 -20
- package/sources/changes-to-specific-files/changes-to-specific-files.mjs +226 -0
- package/sources/changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs +110 -0
- package/sources/common-webhook.mjs +201 -0
- package/sources/new-files-instant/new-files-instant.mjs +95 -0
- package/sources/new-or-modified-comments/new-or-modified-comments.mjs +104 -0
- package/sources/new-or-modified-files/new-or-modified-files.mjs +66 -0
- package/sources/new-or-modified-folders/new-or-modified-folders.mjs +86 -0
- package/sources/new-shared-drive/new-shared-drive.mjs +68 -0
- package/utils.mjs +267 -0
- package/google_drive.app.js +0 -212
- package/sources/changes-to-specific-files/changes-to-specific-files.js +0 -226
- package/sources/new-or-modified-files/new-or-modified-files.js +0 -213
package/package.json
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
"name": "@pipedream/google_drive",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Pipedream Google_drive Components",
|
|
5
|
+
"main": "google_drive.app.mjs",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pipedream",
|
|
8
|
+
"google_drive"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://pipedream.com/apps/google_drive",
|
|
11
|
+
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@googleapis/drive": "^2.3.0",
|
|
15
|
+
"@pipedream/platform": "^0.9.0",
|
|
16
|
+
"axios": "^0.21.1",
|
|
17
|
+
"mime-db": "^1.51.0",
|
|
18
|
+
"uuid": "^8.3.2"
|
|
19
|
+
},
|
|
20
|
+
"gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import cronParser from "cron-parser";
|
|
2
|
+
import includes from "lodash/includes.js";
|
|
3
|
+
import { v4 as uuid } from "uuid";
|
|
4
|
+
|
|
5
|
+
import { MY_DRIVE_VALUE } from "../../constants.mjs";
|
|
6
|
+
|
|
7
|
+
import changesToSpecificFiles from "../changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This source uses the Google Drive API's
|
|
11
|
+
* {@link https://developers.google.com/drive/api/v3/reference/files/watch files: watch}
|
|
12
|
+
* endpoint to subscribe to changes to specific files in the user's drive.
|
|
13
|
+
*/
|
|
14
|
+
export default {
|
|
15
|
+
...changesToSpecificFiles,
|
|
16
|
+
key: "google_drive-changes-to-specific-files",
|
|
17
|
+
name: "Changes to Specific Files",
|
|
18
|
+
description: "Watches for changes to specific files, emitting an event any time a change is made to one of those files. To watch for changes to [shared drive](https://support.google.com/a/users/answer/9310351) files, use the **Changes to Specific Files (Shared Drive)** source instead.",
|
|
19
|
+
version: "0.0.19",
|
|
20
|
+
type: "source",
|
|
21
|
+
// Dedupe events based on the "x-goog-message-number" header for the target channel:
|
|
22
|
+
// https://developers.google.com/drive/api/v3/push#making-watch-requests
|
|
23
|
+
dedupe: "unique",
|
|
24
|
+
props: {
|
|
25
|
+
...changesToSpecificFiles.props,
|
|
26
|
+
drive: {
|
|
27
|
+
type: "string",
|
|
28
|
+
label: "Drive",
|
|
29
|
+
description: "Defaults to `My Drive`. To use a [Shared Drive](https://support.google.com/a/users/answer/9310351), use the **Changes to Specific Files (Shared Drive)** source instead.",
|
|
30
|
+
optional: true,
|
|
31
|
+
default: MY_DRIVE_VALUE,
|
|
32
|
+
},
|
|
33
|
+
updateTypes: {
|
|
34
|
+
propDefinition: [
|
|
35
|
+
changesToSpecificFiles.props.googleDrive,
|
|
36
|
+
"updateTypes",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
hooks: {
|
|
41
|
+
...changesToSpecificFiles.hooks,
|
|
42
|
+
async activate() {
|
|
43
|
+
// Called when a component is created or updated. Handles all the logic
|
|
44
|
+
// for starting and stopping watch notifications tied to the desired
|
|
45
|
+
// files.
|
|
46
|
+
|
|
47
|
+
// You can pass the same channel ID in watch requests for multiple files, so
|
|
48
|
+
// our channel ID is fixed for this component to simplify the state we have to
|
|
49
|
+
// keep track of.
|
|
50
|
+
const channelID = this._getChannelID() || uuid();
|
|
51
|
+
|
|
52
|
+
// Subscriptions are keyed on Google's resourceID, "an opaque value that
|
|
53
|
+
// identifies the watched resource". This value is included in request
|
|
54
|
+
// headers, allowing us to look up the watched resource.
|
|
55
|
+
let subscriptions = this._getSubscriptions() || {};
|
|
56
|
+
|
|
57
|
+
const files = this.files;
|
|
58
|
+
for (const fileID of files) {
|
|
59
|
+
const {
|
|
60
|
+
expiration,
|
|
61
|
+
resourceId,
|
|
62
|
+
} = await this.googleDrive.activateFileHook(
|
|
63
|
+
channelID,
|
|
64
|
+
this.http.endpoint,
|
|
65
|
+
fileID,
|
|
66
|
+
);
|
|
67
|
+
// The fileID must be kept with the subscription metadata so we can
|
|
68
|
+
// renew the watch request for this specific file when it expires.
|
|
69
|
+
subscriptions[resourceId] = {
|
|
70
|
+
expiration,
|
|
71
|
+
fileID,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Save metadata on the subscription so we can stop / renew later
|
|
76
|
+
this._setSubscriptions(subscriptions);
|
|
77
|
+
this._setChannelID(channelID);
|
|
78
|
+
},
|
|
79
|
+
async deactivate() {
|
|
80
|
+
const channelID = this._getChannelID();
|
|
81
|
+
if (!channelID) {
|
|
82
|
+
console.log(
|
|
83
|
+
"Channel not found, cannot stop notifications for non-existent channel",
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const subscriptions = this._getSubscriptions() || {};
|
|
89
|
+
for (const resourceId of Object.keys(subscriptions)) {
|
|
90
|
+
await this.googleDrive.stopNotifications(channelID, resourceId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Reset DB state
|
|
94
|
+
this._setSubscriptions({});
|
|
95
|
+
this._setChannelID(null);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
methods: {
|
|
99
|
+
...changesToSpecificFiles.methods,
|
|
100
|
+
_getSubscriptions() {
|
|
101
|
+
return this.db.get("subscriptions") || {};
|
|
102
|
+
},
|
|
103
|
+
_setSubscriptions(subscriptions) {
|
|
104
|
+
this.db.set("subscriptions", subscriptions);
|
|
105
|
+
},
|
|
106
|
+
_getNextTimerEventTimestamp(event) {
|
|
107
|
+
if (event.cron) {
|
|
108
|
+
return cronParser
|
|
109
|
+
.parseExpression(event.cron)
|
|
110
|
+
.next()
|
|
111
|
+
.getTime();
|
|
112
|
+
}
|
|
113
|
+
if (event.interval_seconds) {
|
|
114
|
+
return Date.now() + event.interval_seconds * 1000;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async renewFileSubscriptions(event) {
|
|
118
|
+
// Assume subscription & channelID may all be undefined at
|
|
119
|
+
// this point Handle their absence appropriately.
|
|
120
|
+
const subscriptions = this._getSubscriptions() || {};
|
|
121
|
+
const channelID = this._getChannelID() || uuid();
|
|
122
|
+
|
|
123
|
+
const nextRunTimestamp = this._getNextTimerEventTimestamp(event);
|
|
124
|
+
|
|
125
|
+
for (const [
|
|
126
|
+
currentResourceId,
|
|
127
|
+
metadata,
|
|
128
|
+
] of Object.entries(subscriptions)) {
|
|
129
|
+
const { fileID } = metadata;
|
|
130
|
+
|
|
131
|
+
const subscription = {
|
|
132
|
+
...metadata,
|
|
133
|
+
resourceId: currentResourceId,
|
|
134
|
+
};
|
|
135
|
+
const {
|
|
136
|
+
expiration,
|
|
137
|
+
resourceId,
|
|
138
|
+
} = await this.googleDrive.renewFileSubscription(
|
|
139
|
+
subscription,
|
|
140
|
+
this.http.endpoint,
|
|
141
|
+
channelID,
|
|
142
|
+
fileID,
|
|
143
|
+
nextRunTimestamp,
|
|
144
|
+
);
|
|
145
|
+
subscriptions[resourceId] = {
|
|
146
|
+
expiration,
|
|
147
|
+
fileID,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
this._setSubscriptions(subscriptions);
|
|
151
|
+
this._setChannelID(channelID);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
async run(event) {
|
|
155
|
+
// This function is polymorphic: it can be triggered as a cron job, to make sure we renew
|
|
156
|
+
// watch requests for specific files, or via HTTP request (the change payloads from Google)
|
|
157
|
+
|
|
158
|
+
// Component was invoked by timer
|
|
159
|
+
if (event.timestamp) {
|
|
160
|
+
return this.renewFileSubscriptions(event);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const channelID = this._getChannelID();
|
|
164
|
+
let subscriptions = this._getSubscriptions() || {};
|
|
165
|
+
|
|
166
|
+
const { headers } = event;
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
!headers["x-goog-resource-state"] ||
|
|
170
|
+
!headers["x-goog-resource-id"] ||
|
|
171
|
+
!headers["x-goog-resource-uri"] ||
|
|
172
|
+
!headers["x-goog-message-number"]
|
|
173
|
+
) {
|
|
174
|
+
console.log("Request missing necessary headers: ", headers);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const incomingChannelID = headers["x-goog-channel-id"];
|
|
179
|
+
if (incomingChannelID !== channelID) {
|
|
180
|
+
console.log(
|
|
181
|
+
`Channel ID of ${incomingChannelID} not equal to deployed component channel of ${channelID}`,
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (subscriptions[headers["x-goog-resource-id"]] === undefined) {
|
|
187
|
+
console.log(
|
|
188
|
+
`Resource ID of ${headers["x-goog-resource-id"]} not currently being tracked. Exiting`,
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!includes(this.updateTypes, headers["x-goog-resource-state"])) {
|
|
194
|
+
console.log(
|
|
195
|
+
`Update type ${headers["x-goog-resource-state"]} not in list of updates to watch: `,
|
|
196
|
+
this.updateTypes,
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// We observed false positives where a single change to a document would trigger two changes:
|
|
202
|
+
// one to "properties" and another to "content,properties". But changes to properties
|
|
203
|
+
// alone are legitimate, most users just won't want this source to emit in those cases.
|
|
204
|
+
// If x-goog-changed is _only_ set to "properties", only move on if the user set the prop
|
|
205
|
+
if (
|
|
206
|
+
!this.watchForPropertiesChanges &&
|
|
207
|
+
headers["x-goog-changed"] === "properties"
|
|
208
|
+
) {
|
|
209
|
+
console.log(
|
|
210
|
+
"Change to properties only, which this component is set to ignore. Exiting",
|
|
211
|
+
);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const file = await this.googleDrive.getFileMetadata(
|
|
216
|
+
headers["x-goog-resource-uri"],
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!file || !Object.keys(file).length) {
|
|
220
|
+
console.log("No file metadata returned, nothing to emit");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.processChange(file, headers);
|
|
225
|
+
},
|
|
226
|
+
};
|
package/sources/changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// This source processes changes to specific files in a user's Google Drive,
|
|
2
|
+
// implementing strategy enumerated in the Push Notifications API docs:
|
|
3
|
+
// https://developers.google.com/drive/api/v3/push .
|
|
4
|
+
//
|
|
5
|
+
// This source has two interfaces:
|
|
6
|
+
//
|
|
7
|
+
// 1) The HTTP requests tied to changes in files in the user's Google Drive
|
|
8
|
+
// 2) A timer that runs on regular intervals, renewing the notification channel as needed
|
|
9
|
+
|
|
10
|
+
import common from "../common-webhook.mjs";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
GOOGLE_DRIVE_NOTIFICATION_CHANGE,
|
|
14
|
+
GOOGLE_DRIVE_NOTIFICATION_ADD,
|
|
15
|
+
GOOGLE_DRIVE_NOTIFICATION_UPDATE,
|
|
16
|
+
} from "../../constants.mjs";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This source uses the Google Drive API's
|
|
20
|
+
* {@link https://developers.google.com/drive/api/v3/reference/changes/watch changes: watch}
|
|
21
|
+
* endpoint to subscribe to changes to the user's drive or a shard drive.
|
|
22
|
+
*/
|
|
23
|
+
export default {
|
|
24
|
+
...common,
|
|
25
|
+
key: "google_drive-changes-to-specific-files-shared-drive",
|
|
26
|
+
name: "Changes to Specific Files (Shared Drive)",
|
|
27
|
+
description: "Watches for changes to specific files in a shared drive, emitting an event any time a change is made to one of those files",
|
|
28
|
+
version: "0.0.1",
|
|
29
|
+
type: "source",
|
|
30
|
+
// Dedupe events based on the "x-goog-message-number" header for the target channel:
|
|
31
|
+
// https://developers.google.com/drive/api/v3/push#making-watch-requests
|
|
32
|
+
dedupe: "unique",
|
|
33
|
+
props: {
|
|
34
|
+
...common.props,
|
|
35
|
+
files: {
|
|
36
|
+
type: "string[]",
|
|
37
|
+
label: "Files",
|
|
38
|
+
description: "The files you want to watch for changes.",
|
|
39
|
+
optional: true,
|
|
40
|
+
default: [],
|
|
41
|
+
options({ prevContext }) {
|
|
42
|
+
const { nextPageToken } = prevContext;
|
|
43
|
+
const baseOpts = {};
|
|
44
|
+
const opts = this.isMyDrive()
|
|
45
|
+
? baseOpts
|
|
46
|
+
: {
|
|
47
|
+
...baseOpts,
|
|
48
|
+
corpora: "drive",
|
|
49
|
+
driveId: this.getDriveId(),
|
|
50
|
+
includeItemsFromAllDrives: true,
|
|
51
|
+
supportsAllDrives: true,
|
|
52
|
+
};
|
|
53
|
+
return this.googleDrive.listFilesOptions(nextPageToken, opts);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
methods: {
|
|
58
|
+
...common.methods,
|
|
59
|
+
getUpdateTypes() {
|
|
60
|
+
return [
|
|
61
|
+
GOOGLE_DRIVE_NOTIFICATION_ADD,
|
|
62
|
+
GOOGLE_DRIVE_NOTIFICATION_CHANGE,
|
|
63
|
+
GOOGLE_DRIVE_NOTIFICATION_UPDATE,
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
generateMeta(data, headers) {
|
|
67
|
+
const {
|
|
68
|
+
id: fileId,
|
|
69
|
+
name: fileName,
|
|
70
|
+
modifiedTime: tsString,
|
|
71
|
+
} = data;
|
|
72
|
+
const {
|
|
73
|
+
"x-goog-message-number": eventId,
|
|
74
|
+
"x-goog-resource-state": resourceState,
|
|
75
|
+
} = headers;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: `${fileId}-${eventId}`,
|
|
79
|
+
summary: `${resourceState.toUpperCase()} - ${
|
|
80
|
+
fileName || "Untitled"
|
|
81
|
+
}`,
|
|
82
|
+
ts: Date.parse(tsString),
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
isFileRelevant(file) {
|
|
86
|
+
return this.files.includes(file.id);
|
|
87
|
+
},
|
|
88
|
+
async processChange(file, headers) {
|
|
89
|
+
const eventToEmit = {
|
|
90
|
+
file,
|
|
91
|
+
change: {
|
|
92
|
+
state: headers["x-goog-resource-state"],
|
|
93
|
+
resourceURI: headers["x-goog-resource-uri"],
|
|
94
|
+
changed: headers["x-goog-changed"], // "Additional details about the changes. Possible values: content, parents, children, permissions"
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const meta = this.generateMeta(file, headers);
|
|
98
|
+
this.$emit(eventToEmit, meta);
|
|
99
|
+
},
|
|
100
|
+
async processChanges(changedFiles, headers) {
|
|
101
|
+
for (const file of changedFiles) {
|
|
102
|
+
if (!this.isFileRelevant(file)) {
|
|
103
|
+
console.log(`Skipping event for irrelevant file ${file.id}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
this.processChange(file, headers);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import includes from "lodash/includes.js";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
|
|
4
|
+
import googleDrive from "../google_drive.app.mjs";
|
|
5
|
+
import { WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS } from "../constants.mjs";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
props: {
|
|
9
|
+
googleDrive,
|
|
10
|
+
db: "$.service.db",
|
|
11
|
+
http: "$.interface.http",
|
|
12
|
+
drive: {
|
|
13
|
+
propDefinition: [
|
|
14
|
+
googleDrive,
|
|
15
|
+
"watchedDrive",
|
|
16
|
+
],
|
|
17
|
+
description: "Defaults to My Drive. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.",
|
|
18
|
+
optional: false,
|
|
19
|
+
},
|
|
20
|
+
watchForPropertiesChanges: {
|
|
21
|
+
propDefinition: [
|
|
22
|
+
googleDrive,
|
|
23
|
+
"watchForPropertiesChanges",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
timer: {
|
|
27
|
+
label: "Push notification renewal schedule",
|
|
28
|
+
description:
|
|
29
|
+
"The Google Drive API requires occasional renewal of push notification subscriptions. **This runs in the background, so you should not need to modify this schedule**.",
|
|
30
|
+
type: "$.interface.timer",
|
|
31
|
+
static: {
|
|
32
|
+
intervalSeconds: WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
hooks: {
|
|
37
|
+
async activate() {
|
|
38
|
+
// Called when a component is created or updated. Handles all the logic
|
|
39
|
+
// for starting and stopping watch notifications tied to the desired
|
|
40
|
+
// files.
|
|
41
|
+
const channelID = uuid();
|
|
42
|
+
const {
|
|
43
|
+
startPageToken,
|
|
44
|
+
expiration,
|
|
45
|
+
resourceId,
|
|
46
|
+
} = await this.googleDrive.activateHook(
|
|
47
|
+
channelID,
|
|
48
|
+
this.http.endpoint,
|
|
49
|
+
this.getDriveId(),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// We use and increment the pageToken as new changes arrive, in run()
|
|
53
|
+
this._setPageToken(startPageToken);
|
|
54
|
+
|
|
55
|
+
// Save metadata on the subscription so we can stop / renew later
|
|
56
|
+
// Subscriptions are tied to Google's resourceID, "an opaque value that
|
|
57
|
+
// identifies the watched resource". This value is included in request headers
|
|
58
|
+
this._setSubscription({
|
|
59
|
+
resourceId,
|
|
60
|
+
expiration,
|
|
61
|
+
});
|
|
62
|
+
this._setChannelID(channelID);
|
|
63
|
+
},
|
|
64
|
+
async deactivate() {
|
|
65
|
+
const channelID = this._getChannelID();
|
|
66
|
+
const { resourceId } = this._getSubscription();
|
|
67
|
+
await this.googleDrive.deactivateHook(channelID, resourceId);
|
|
68
|
+
|
|
69
|
+
this._setSubscription(null);
|
|
70
|
+
this._setChannelID(null);
|
|
71
|
+
this._setPageToken(null);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
methods: {
|
|
75
|
+
_getSubscription() {
|
|
76
|
+
return this.db.get("subscription");
|
|
77
|
+
},
|
|
78
|
+
_setSubscription(subscription) {
|
|
79
|
+
this.db.set("subscription", subscription);
|
|
80
|
+
},
|
|
81
|
+
_getChannelID() {
|
|
82
|
+
return this.db.get("channelID");
|
|
83
|
+
},
|
|
84
|
+
_setChannelID(channelID) {
|
|
85
|
+
this.db.set("channelID", channelID);
|
|
86
|
+
},
|
|
87
|
+
_getPageToken() {
|
|
88
|
+
return this.db.get("pageToken");
|
|
89
|
+
},
|
|
90
|
+
_setPageToken(pageToken) {
|
|
91
|
+
this.db.set("pageToken", pageToken);
|
|
92
|
+
},
|
|
93
|
+
isMyDrive(drive = this.drive) {
|
|
94
|
+
return googleDrive.methods.isMyDrive(drive);
|
|
95
|
+
},
|
|
96
|
+
getDriveId(drive = this.drive) {
|
|
97
|
+
return googleDrive.methods.getDriveId(drive);
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* This method returns the types of updates/events from Google Drive that
|
|
101
|
+
* the event source should listen to. This base implementation returns an
|
|
102
|
+
* empty list, which means that any event source that extends this module
|
|
103
|
+
* and that does not refine this implementation will essentially ignore
|
|
104
|
+
* every incoming event from Google Drive.
|
|
105
|
+
*
|
|
106
|
+
* @returns
|
|
107
|
+
* @type {UpdateType[]}
|
|
108
|
+
*/
|
|
109
|
+
getUpdateTypes() {
|
|
110
|
+
return [];
|
|
111
|
+
},
|
|
112
|
+
/**
|
|
113
|
+
* This method is responsible for processing a list of changed files
|
|
114
|
+
* according to the event source's purpose. As an abstract method, it must
|
|
115
|
+
* be implemented by every event source that extends this module.
|
|
116
|
+
*
|
|
117
|
+
* @param {object[]} [changedFiles] - the list of file changes, as [defined
|
|
118
|
+
* by the API](https://bit.ly/3h7WeUa)
|
|
119
|
+
* @param {object} [headers] - an object containing the request headers of
|
|
120
|
+
* the webhook call made by Google Drive
|
|
121
|
+
*/
|
|
122
|
+
processChanges() {
|
|
123
|
+
throw new Error("processChanges is not implemented");
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
async run(event) {
|
|
127
|
+
// This function is polymorphic: it can be triggered as a cron job, to make
|
|
128
|
+
// sure we renew watch requests for specific files, or via HTTP request (the
|
|
129
|
+
// change payloads from Google)
|
|
130
|
+
const subscription = this._getSubscription();
|
|
131
|
+
const channelID = this._getChannelID();
|
|
132
|
+
const pageToken = this._getPageToken();
|
|
133
|
+
|
|
134
|
+
// Component was invoked by timer
|
|
135
|
+
if (event.timestamp) {
|
|
136
|
+
const {
|
|
137
|
+
newChannelID,
|
|
138
|
+
newPageToken,
|
|
139
|
+
expiration,
|
|
140
|
+
resourceId,
|
|
141
|
+
} = await this.googleDrive.renewSubscription(
|
|
142
|
+
this.drive,
|
|
143
|
+
subscription,
|
|
144
|
+
this.http.endpoint,
|
|
145
|
+
channelID,
|
|
146
|
+
pageToken,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
this._setSubscription({
|
|
150
|
+
expiration,
|
|
151
|
+
resourceId,
|
|
152
|
+
});
|
|
153
|
+
this._setChannelID(newChannelID);
|
|
154
|
+
this._setPageToken(newPageToken);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { headers } = event;
|
|
159
|
+
if (!this.googleDrive.checkHeaders(headers, subscription, channelID)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!includes(this.getUpdateTypes(), headers["x-goog-resource-state"])) {
|
|
164
|
+
console.log(
|
|
165
|
+
`Update type ${headers["x-goog-resource-state"]} not in list of updates to watch: `,
|
|
166
|
+
this.getUpdateTypes(),
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// We observed false positives where a single change to a document would trigger two changes:
|
|
172
|
+
// one to "properties" and another to "content,properties". But changes to properties
|
|
173
|
+
// alone are legitimate, most users just won't want this source to emit in those cases.
|
|
174
|
+
// If x-goog-changed is _only_ set to "properties", only move on if the user set the prop
|
|
175
|
+
if (
|
|
176
|
+
!this.watchForPropertiesChanges &&
|
|
177
|
+
headers["x-goog-changed"] === "properties"
|
|
178
|
+
) {
|
|
179
|
+
console.log(
|
|
180
|
+
"Change to properties only, which this component is set to ignore. Exiting",
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const driveId = this.getDriveId();
|
|
186
|
+
const changedFilesStream = this.googleDrive.listChanges(pageToken, driveId);
|
|
187
|
+
for await (const changedFilesPage of changedFilesStream) {
|
|
188
|
+
const {
|
|
189
|
+
changedFiles,
|
|
190
|
+
nextPageToken,
|
|
191
|
+
} = changedFilesPage;
|
|
192
|
+
|
|
193
|
+
// Process all the changed files retrieved from the current page
|
|
194
|
+
await this.processChanges(changedFiles, headers);
|
|
195
|
+
|
|
196
|
+
// After successfully processing the changed files, we store the page
|
|
197
|
+
// token of the next page
|
|
198
|
+
this._setPageToken(nextPageToken);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import common from "../common-webhook.mjs";
|
|
2
|
+
import {
|
|
3
|
+
GOOGLE_DRIVE_NOTIFICATION_ADD,
|
|
4
|
+
GOOGLE_DRIVE_NOTIFICATION_CHANGE,
|
|
5
|
+
} from "../../constants.mjs";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
...common,
|
|
9
|
+
key: "google_drive-new-files-instant",
|
|
10
|
+
name: "New Files (Instant)",
|
|
11
|
+
description: "Emit new event any time a new file is added in your linked Google Drive",
|
|
12
|
+
version: "0.0.15",
|
|
13
|
+
type: "source",
|
|
14
|
+
dedupe: "unique",
|
|
15
|
+
props: {
|
|
16
|
+
...common.props,
|
|
17
|
+
folders: {
|
|
18
|
+
type: "string[]",
|
|
19
|
+
label: "Folders",
|
|
20
|
+
description:
|
|
21
|
+
"(Optional) The folders you want to watch for changes. Leave blank to watch for any new file in the Drive.",
|
|
22
|
+
optional: true,
|
|
23
|
+
default: [],
|
|
24
|
+
options({ prevContext }) {
|
|
25
|
+
const { nextPageToken } = prevContext;
|
|
26
|
+
const baseOpts = {
|
|
27
|
+
q: "mimeType = 'application/vnd.google-apps.folder'",
|
|
28
|
+
};
|
|
29
|
+
const opts = this.isMyDrive()
|
|
30
|
+
? baseOpts
|
|
31
|
+
: {
|
|
32
|
+
...baseOpts,
|
|
33
|
+
corpora: "drive",
|
|
34
|
+
driveId: this.getDriveId(),
|
|
35
|
+
includeItemsFromAllDrives: true,
|
|
36
|
+
supportsAllDrives: true,
|
|
37
|
+
};
|
|
38
|
+
return this.googleDrive.listFilesOptions(nextPageToken, opts);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
hooks: {
|
|
43
|
+
...common.hooks,
|
|
44
|
+
async activate() {
|
|
45
|
+
await common.hooks.activate.bind(this)();
|
|
46
|
+
this._setLastFileCreatedTime(Date.now());
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
methods: {
|
|
50
|
+
...common.methods,
|
|
51
|
+
_getLastFileCreatedTime() {
|
|
52
|
+
return this.db.get("lastFileCreatedTime");
|
|
53
|
+
},
|
|
54
|
+
_setLastFileCreatedTime(lastFileCreatedTime) {
|
|
55
|
+
this.db.set("lastFileCreatedTime", lastFileCreatedTime);
|
|
56
|
+
},
|
|
57
|
+
shouldProcess(file) {
|
|
58
|
+
const watchedFolders = new Set(this.folders);
|
|
59
|
+
return (
|
|
60
|
+
watchedFolders.size == 0 ||
|
|
61
|
+
(file.parents && file.parents.some((p) => watchedFolders.has(p)))
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
getUpdateTypes() {
|
|
65
|
+
return [
|
|
66
|
+
GOOGLE_DRIVE_NOTIFICATION_ADD,
|
|
67
|
+
GOOGLE_DRIVE_NOTIFICATION_CHANGE,
|
|
68
|
+
];
|
|
69
|
+
},
|
|
70
|
+
async processChanges(changedFiles) {
|
|
71
|
+
const lastFileCreatedTime = this._getLastFileCreatedTime();
|
|
72
|
+
let maxCreatedTime = lastFileCreatedTime;
|
|
73
|
+
|
|
74
|
+
for (const file of changedFiles) {
|
|
75
|
+
const fileInfo = await this.googleDrive.getFile(file.id);
|
|
76
|
+
const createdTime = Date.parse(fileInfo.createdTime);
|
|
77
|
+
if (
|
|
78
|
+
!this.shouldProcess(fileInfo) ||
|
|
79
|
+
createdTime < lastFileCreatedTime
|
|
80
|
+
) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.$emit(fileInfo, {
|
|
85
|
+
summary: `New File: ${fileInfo.name}`,
|
|
86
|
+
id: file.id,
|
|
87
|
+
ts: createdTime,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
maxCreatedTime = Math.max(createdTime, maxCreatedTime);
|
|
91
|
+
this._setLastFileCreatedTime(maxCreatedTime);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|