@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/google_drive.app.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
const axios = require("axios");
|
|
2
|
-
const { google } = require("googleapis");
|
|
3
|
-
|
|
4
|
-
const GOOGLE_DRIVE_UPDATE_TYPES = [
|
|
5
|
-
"add",
|
|
6
|
-
"sync",
|
|
7
|
-
"remove",
|
|
8
|
-
"update",
|
|
9
|
-
"trash",
|
|
10
|
-
"untrash",
|
|
11
|
-
"change",
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
module.exports = {
|
|
15
|
-
type: "app",
|
|
16
|
-
app: "google_drive",
|
|
17
|
-
propDefinitions: {
|
|
18
|
-
watchedDrive: {
|
|
19
|
-
type: "string",
|
|
20
|
-
label: "Drive",
|
|
21
|
-
description: "The drive you want to watch for changes",
|
|
22
|
-
async options({ page, prevContext }) {
|
|
23
|
-
const { nextPageToken } = prevContext;
|
|
24
|
-
return await this.listDrives(nextPageToken);
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
updateTypes: {
|
|
28
|
-
type: "string[]",
|
|
29
|
-
label: "Types of updates",
|
|
30
|
-
description:
|
|
31
|
-
"The types of updates you want to watch for on these files. [See Google's docs](https://developers.google.com/drive/api/v3/push#understanding-drive-api-notification-events).",
|
|
32
|
-
// https://developers.google.com/drive/api/v3/push#understanding-drive-api-notification-events
|
|
33
|
-
default: GOOGLE_DRIVE_UPDATE_TYPES,
|
|
34
|
-
options: GOOGLE_DRIVE_UPDATE_TYPES,
|
|
35
|
-
},
|
|
36
|
-
watchForPropertiesChanges: {
|
|
37
|
-
type: "boolean",
|
|
38
|
-
label: "Watch for changes to file properties",
|
|
39
|
-
description:
|
|
40
|
-
"Watch for changes to [file properties](https://developers.google.com/drive/api/v3/properties) in addition to changes to content. **Defaults to `false`, watching for only changes to content**.",
|
|
41
|
-
optional: true,
|
|
42
|
-
default: false,
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
methods: {
|
|
46
|
-
// Returns a drive object authenticated with the user's access token
|
|
47
|
-
drive() {
|
|
48
|
-
const auth = new google.auth.OAuth2();
|
|
49
|
-
auth.setCredentials({ access_token: this.$auth.oauth_access_token });
|
|
50
|
-
return google.drive({ version: "v3", auth });
|
|
51
|
-
},
|
|
52
|
-
// Google's push notifications provide a URL to the resource that changed,
|
|
53
|
-
// which we can use to fetch the file's metadata. So we use axios here
|
|
54
|
-
// (vs. the Node client) to get that.
|
|
55
|
-
async getFileMetadata(url) {
|
|
56
|
-
return (
|
|
57
|
-
await axios({
|
|
58
|
-
method: "GET",
|
|
59
|
-
headers: {
|
|
60
|
-
Authorization: `Bearer ${this.$auth.oauth_access_token}`,
|
|
61
|
-
},
|
|
62
|
-
url,
|
|
63
|
-
})
|
|
64
|
-
).data;
|
|
65
|
-
},
|
|
66
|
-
async getChanges(pageToken, driveId) {
|
|
67
|
-
const drive = this.drive();
|
|
68
|
-
// As with many of the methods for Google Drive, we must
|
|
69
|
-
// pass a request of a different shape when we're requesting
|
|
70
|
-
// changes for My Drive (null driveId) vs. a shared drive
|
|
71
|
-
let changeRequest;
|
|
72
|
-
if (driveId) {
|
|
73
|
-
changeRequest = {
|
|
74
|
-
driveId,
|
|
75
|
-
pageToken,
|
|
76
|
-
includeItemsFromAllDrives: true,
|
|
77
|
-
supportsAllDrives: true,
|
|
78
|
-
};
|
|
79
|
-
} else {
|
|
80
|
-
changeRequest = {
|
|
81
|
-
pageToken,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
const { changes, newStartPageToken } = (
|
|
85
|
-
await drive.changes.list(changeRequest)
|
|
86
|
-
).data;
|
|
87
|
-
|
|
88
|
-
// Some changes do not include an associated file object. Return only those that do
|
|
89
|
-
const changedFiles = changes
|
|
90
|
-
.map((change) => change.file)
|
|
91
|
-
.filter((f) => typeof f === "object");
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
changedFiles,
|
|
95
|
-
newStartPageToken,
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
async getPageToken() {
|
|
99
|
-
const drive = this.drive();
|
|
100
|
-
return (await drive.changes.getStartPageToken({})).data.startPageToken;
|
|
101
|
-
},
|
|
102
|
-
async listDrives(pageToken) {
|
|
103
|
-
const drive = this.drive();
|
|
104
|
-
const resp = await drive.drives.list({ pageToken });
|
|
105
|
-
const { drives, nextPageToken } = resp.data;
|
|
106
|
-
// "My Drive" isn't returned from the list of drives,
|
|
107
|
-
// so we add it to the list and assign it a static
|
|
108
|
-
// ID that we can refer to when we need.
|
|
109
|
-
const options = [{ label: "My Drive", value: "myDrive" }];
|
|
110
|
-
for (const d of drives) {
|
|
111
|
-
options.push({ label: d.name, value: d.id });
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
options,
|
|
115
|
-
context: { nextPageToken },
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
async listFiles(opts) {
|
|
119
|
-
const drive = this.drive();
|
|
120
|
-
// Listing files in My Drive and a shared drive requires
|
|
121
|
-
// mutually-exclusive options, so we accept an object of
|
|
122
|
-
// opts from the caller and pass them to the list method.
|
|
123
|
-
const resp = await drive.files.list(opts);
|
|
124
|
-
const { files, nextPageToken } = resp.data;
|
|
125
|
-
const options = files.map((file) => {
|
|
126
|
-
return { label: file.name, value: file.id };
|
|
127
|
-
});
|
|
128
|
-
return {
|
|
129
|
-
options,
|
|
130
|
-
context: { nextPageToken },
|
|
131
|
-
};
|
|
132
|
-
},
|
|
133
|
-
_makeWatchRequestBody(id, address) {
|
|
134
|
-
return {
|
|
135
|
-
id, // the component-specific channel ID, a UUID
|
|
136
|
-
type: "web_hook",
|
|
137
|
-
address, // the component-specific HTTP endpoint
|
|
138
|
-
};
|
|
139
|
-
},
|
|
140
|
-
async watchDrive(id, address, pageToken, driveId) {
|
|
141
|
-
const drive = this.drive();
|
|
142
|
-
const requestBody = this._makeWatchRequestBody(id, address);
|
|
143
|
-
|
|
144
|
-
// Google expects an entirely different object to be passed
|
|
145
|
-
// when you make a watch request for My Drive vs. a shared drive
|
|
146
|
-
// "My Drive" doesn't have a driveId, so if this method is called
|
|
147
|
-
// without a driveId, we make a watch request for My Drive
|
|
148
|
-
let watchRequest;
|
|
149
|
-
if (driveId) {
|
|
150
|
-
watchRequest = {
|
|
151
|
-
driveId,
|
|
152
|
-
pageToken,
|
|
153
|
-
requestBody,
|
|
154
|
-
includeItemsFromAllDrives: true,
|
|
155
|
-
supportsAllDrives: true,
|
|
156
|
-
};
|
|
157
|
-
} else {
|
|
158
|
-
watchRequest = {
|
|
159
|
-
pageToken,
|
|
160
|
-
requestBody,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
// When watching for changes to an entire account, we must pass a pageToken,
|
|
164
|
-
// which points to the moment in time we want to start watching for changes:
|
|
165
|
-
// https://developers.google.com/drive/api/v3/manage-changes
|
|
166
|
-
const { expiration, resourceId } = (
|
|
167
|
-
await drive.changes.watch(watchRequest)
|
|
168
|
-
).data;
|
|
169
|
-
console.log(`Watch request for drive successful, expiry: ${expiration}`);
|
|
170
|
-
return {
|
|
171
|
-
expiration: parseInt(expiration),
|
|
172
|
-
resourceId,
|
|
173
|
-
};
|
|
174
|
-
},
|
|
175
|
-
async watchFile(id, address, fileId) {
|
|
176
|
-
const drive = this.drive();
|
|
177
|
-
const requestBody = this._makeWatchRequestBody(id, address);
|
|
178
|
-
const { expiration, resourceId } = (
|
|
179
|
-
await drive.files.watch({
|
|
180
|
-
fileId,
|
|
181
|
-
requestBody,
|
|
182
|
-
supportsAllDrives: true,
|
|
183
|
-
})
|
|
184
|
-
).data;
|
|
185
|
-
console.log(
|
|
186
|
-
`Watch request for file ${fileId} successful, expiry: ${expiration}`
|
|
187
|
-
);
|
|
188
|
-
return {
|
|
189
|
-
expiration: parseInt(expiration),
|
|
190
|
-
resourceId,
|
|
191
|
-
};
|
|
192
|
-
},
|
|
193
|
-
async stopNotifications(id, resourceId) {
|
|
194
|
-
// id = channelID
|
|
195
|
-
// See https://github.com/googleapis/google-api-nodejs-client/issues/627
|
|
196
|
-
const drive = this.drive();
|
|
197
|
-
|
|
198
|
-
// If for some reason the channel doesn't exist, this throws an error
|
|
199
|
-
// It's OK for this to fail in those cases, since we'll renew the channel
|
|
200
|
-
// immediately after trying to stop it if we still want notifications,
|
|
201
|
-
// so we squash the error, log it, and move on.
|
|
202
|
-
try {
|
|
203
|
-
await drive.channels.stop({ resource: { id, resourceId } });
|
|
204
|
-
console.log(`Stopped push notifications on channel ${id}`);
|
|
205
|
-
} catch (err) {
|
|
206
|
-
console.error(
|
|
207
|
-
`Failed to stop channel ${id} for resource ${resourceId}: ${err}`
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
};
|
|
@@ -1,226 +0,0 @@
|
|
|
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 the user's Google Drive
|
|
8
|
-
// 2) A timer that runs on regular intervals, renewing the notification channel as needed
|
|
9
|
-
|
|
10
|
-
const { uuid } = require("uuidv4");
|
|
11
|
-
const includes = require("lodash.includes");
|
|
12
|
-
const googleDrive = require("../../google_drive.app.js");
|
|
13
|
-
|
|
14
|
-
module.exports = {
|
|
15
|
-
key: "google_drive-changes-to-specific-files",
|
|
16
|
-
name: "Changes to Specific Files",
|
|
17
|
-
description:
|
|
18
|
-
"Watches for changes to specific files, emitting an event any time a change is made to one of those files",
|
|
19
|
-
version: "0.0.6",
|
|
20
|
-
// Dedupe events based on the "x-goog-message-number" header for the target channel:
|
|
21
|
-
// https://developers.google.com/drive/api/v3/push#making-watch-requests
|
|
22
|
-
dedupe: "unique",
|
|
23
|
-
props: {
|
|
24
|
-
googleDrive,
|
|
25
|
-
db: "$.service.db",
|
|
26
|
-
http: "$.interface.http",
|
|
27
|
-
drive: { propDefinition: [googleDrive, "watchedDrive"] },
|
|
28
|
-
files: {
|
|
29
|
-
type: "string[]",
|
|
30
|
-
label: "Files",
|
|
31
|
-
description: "The files you want to watch for changes.",
|
|
32
|
-
optional: true,
|
|
33
|
-
async options({ page, prevContext }) {
|
|
34
|
-
const { nextPageToken } = prevContext;
|
|
35
|
-
if (!this.drive) return [];
|
|
36
|
-
if (this.drive === "myDrive") {
|
|
37
|
-
return await this.googleDrive.listFiles({ pageToken: nextPageToken });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return await this.googleDrive.listFiles({
|
|
41
|
-
pageToken: nextPageToken,
|
|
42
|
-
corpora: "drive",
|
|
43
|
-
driveId: this.drive,
|
|
44
|
-
includeItemsFromAllDrives: true,
|
|
45
|
-
supportsAllDrives: true,
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
updateTypes: { propDefinition: [googleDrive, "updateTypes"] },
|
|
50
|
-
watchForPropertiesChanges: {
|
|
51
|
-
propDefinition: [googleDrive, "watchForPropertiesChanges"],
|
|
52
|
-
},
|
|
53
|
-
timer: {
|
|
54
|
-
label: "Push notification renewal schedule",
|
|
55
|
-
description:
|
|
56
|
-
"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**.",
|
|
57
|
-
type: "$.interface.timer",
|
|
58
|
-
default: {
|
|
59
|
-
intervalSeconds: 60 * 60 * 24,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
hooks: {
|
|
64
|
-
async activate() {
|
|
65
|
-
// Called when a componenent is created or updated. Handles all the logic
|
|
66
|
-
// for starting and stopping watch notifications tied to the desired files.
|
|
67
|
-
|
|
68
|
-
// You can pass the same channel ID in watch requests for multiple files, so
|
|
69
|
-
// our channel ID is fixed for this component to simplify the state we have to
|
|
70
|
-
// keep track of.
|
|
71
|
-
const channelID = this.db.get("channelID") || uuid();
|
|
72
|
-
|
|
73
|
-
// Subscriptions are keyed on Google's resourceID, "an opaque value that
|
|
74
|
-
// identifies the watched resource". This value is included in request
|
|
75
|
-
// headers, allowing us to look up the watched resource.
|
|
76
|
-
let subscriptions = this.db.get("subscriptions") || {};
|
|
77
|
-
|
|
78
|
-
for (const fileID of this.files) {
|
|
79
|
-
const { expiration, resourceId } = await this.googleDrive.watchFile(
|
|
80
|
-
channelID,
|
|
81
|
-
this.http.endpoint,
|
|
82
|
-
fileID
|
|
83
|
-
);
|
|
84
|
-
// The fileID must be kept with the subscription metadata so we can
|
|
85
|
-
// renew the watch request for this specific file when it expires.
|
|
86
|
-
subscriptions[resourceId] = { expiration, fileID };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Save metadata on the subscription so we can stop / renew later
|
|
90
|
-
this.db.set("subscriptions", subscriptions);
|
|
91
|
-
this.db.set("channelID", channelID);
|
|
92
|
-
},
|
|
93
|
-
async deactivate() {
|
|
94
|
-
const channelID = this.db.get("channelID");
|
|
95
|
-
if (!channelID) {
|
|
96
|
-
console.log(
|
|
97
|
-
"Channel not found, cannot stop notifications for non-existent channel"
|
|
98
|
-
);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const subscriptions = this.db.get("subscriptions") || {};
|
|
103
|
-
for (const resourceId of Object.keys(subscriptions)) {
|
|
104
|
-
await this.googleDrive.stopNotifications(channelID, resourceId);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Reset DB state
|
|
108
|
-
this.db.set("subscriptions", {});
|
|
109
|
-
this.db.set("channelID", null);
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
async run(event) {
|
|
113
|
-
// This function is polymorphic: it can be triggered as a cron job, to make sure we renew
|
|
114
|
-
// watch requests for specific files, or via HTTP request (the change payloads from Google)
|
|
115
|
-
|
|
116
|
-
let subscriptions = this.db.get("subscriptions") || {};
|
|
117
|
-
const channelID = this.db.get("channelID");
|
|
118
|
-
|
|
119
|
-
// Component was invoked by timer
|
|
120
|
-
if (event.interval_seconds) {
|
|
121
|
-
for (const [currentResourceId, metadata] of Object.entries(
|
|
122
|
-
subscriptions
|
|
123
|
-
)) {
|
|
124
|
-
const { fileID } = metadata;
|
|
125
|
-
// If the subscription for this resource will expire before the next run,
|
|
126
|
-
// stop the existing subscription and renew
|
|
127
|
-
if (metadata.expiration < +new Date() + event.interval_seconds * 1000) {
|
|
128
|
-
console.log(
|
|
129
|
-
`Notifications for resource ${currentResourceId} are expiring at ${metadata.expiration}. Renewing`
|
|
130
|
-
);
|
|
131
|
-
await this.googleDrive.stopNotifications(
|
|
132
|
-
channelID,
|
|
133
|
-
currentResourceId
|
|
134
|
-
);
|
|
135
|
-
const { expiration, resourceId } = await this.googleDrive.watchFile(
|
|
136
|
-
channelID,
|
|
137
|
-
this.http.endpoint,
|
|
138
|
-
fileID
|
|
139
|
-
);
|
|
140
|
-
subscriptions[resourceId] = { expiration, fileID };
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.db.set("subscriptions", subscriptions);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const { headers } = event;
|
|
149
|
-
|
|
150
|
-
if (headers["x-goog-resource-state"] === "sync") {
|
|
151
|
-
console.log("Sync notification, exiting early");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
!headers["x-goog-resource-state"] ||
|
|
157
|
-
!headers["x-goog-resource-id"] ||
|
|
158
|
-
!headers["x-goog-resource-uri"] ||
|
|
159
|
-
!headers["x-goog-message-number"]
|
|
160
|
-
) {
|
|
161
|
-
console.log("Request missing necessary headers: ", headers);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const incomingChannelID = headers["x-goog-channel-id"];
|
|
166
|
-
if (incomingChannelID !== channelID) {
|
|
167
|
-
console.log(
|
|
168
|
-
`Channel ID of ${incomingChannelID} not equal to deployed component channel of ${channelID}`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!(headers["x-goog-resource-id"] in subscriptions)) {
|
|
173
|
-
console.log(
|
|
174
|
-
`Resource ID of ${resourceId} not currently being tracked. Exiting`
|
|
175
|
-
);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (!includes(this.updateTypes, headers["x-goog-resource-state"])) {
|
|
180
|
-
console.log(
|
|
181
|
-
`Update type ${headers["x-goog-resource-state"]} not in list of updates to watch: `,
|
|
182
|
-
this.updateTypes
|
|
183
|
-
);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// We observed false positives where a single change to a document would trigger two changes:
|
|
188
|
-
// one to "properties" and another to "content,properties". But changes to properties
|
|
189
|
-
// alone are legitimate, most users just won't want this source to emit in those cases.
|
|
190
|
-
// If x-goog-changed is _only_ set to "properties", only move on if the user set the prop
|
|
191
|
-
if (
|
|
192
|
-
!this.watchForPropertiesChanges &&
|
|
193
|
-
headers["x-goog-changed"] === "properties"
|
|
194
|
-
) {
|
|
195
|
-
console.log(
|
|
196
|
-
"Change to properties only, which this component is set to ignore. Exiting"
|
|
197
|
-
);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const file = await this.googleDrive.getFileMetadata(
|
|
202
|
-
headers["x-goog-resource-uri"]
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
if (!file || !Object.keys(file).length) {
|
|
206
|
-
console.log("No file metadata returned, nothing to emit");
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const eventToEmit = {
|
|
211
|
-
file,
|
|
212
|
-
change: {
|
|
213
|
-
state: headers["x-goog-resource-state"],
|
|
214
|
-
resourceURI: headers["x-goog-resource-uri"],
|
|
215
|
-
changed: headers["x-goog-changed"], // "Additional details about the changes. Possible values: content, parents, children, permissions"
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
this.$emit(eventToEmit, {
|
|
220
|
-
summary: `${headers["x-goog-resource-state"].toUpperCase()} - ${
|
|
221
|
-
file.name || "Untitled"
|
|
222
|
-
}`,
|
|
223
|
-
id: headers["x-goog-message-number"],
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
};
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// This source processes changes to any 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 and here:
|
|
4
|
-
// https://developers.google.com/drive/api/v3/manage-changes
|
|
5
|
-
//
|
|
6
|
-
// This source has two interfaces:
|
|
7
|
-
//
|
|
8
|
-
// 1) The HTTP requests tied to changes in the user's Google Drive
|
|
9
|
-
// 2) A timer that runs on regular intervals, renewing the notification channel as needed
|
|
10
|
-
|
|
11
|
-
const { uuid } = require("uuidv4");
|
|
12
|
-
const includes = require("lodash.includes");
|
|
13
|
-
const googleDrive = require("../../google_drive.app.js");
|
|
14
|
-
|
|
15
|
-
module.exports = {
|
|
16
|
-
key: "google_drive-new-or-modified-files",
|
|
17
|
-
name: "New or Modified Files",
|
|
18
|
-
description:
|
|
19
|
-
"Emits a new event any time any file in your linked Google Drive is added, modified, or deleted",
|
|
20
|
-
version: "0.0.6",
|
|
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
|
-
googleDrive,
|
|
26
|
-
db: "$.service.db",
|
|
27
|
-
http: "$.interface.http",
|
|
28
|
-
drive: { propDefinition: [googleDrive, "watchedDrive"] },
|
|
29
|
-
updateTypes: { propDefinition: [googleDrive, "updateTypes"] },
|
|
30
|
-
watchForPropertiesChanges: {
|
|
31
|
-
propDefinition: [googleDrive, "watchForPropertiesChanges"],
|
|
32
|
-
},
|
|
33
|
-
timer: {
|
|
34
|
-
label: "Push notification renewal schedule",
|
|
35
|
-
description:
|
|
36
|
-
"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**.",
|
|
37
|
-
type: "$.interface.timer",
|
|
38
|
-
default: {
|
|
39
|
-
intervalSeconds: 60 * 60 * 24,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
hooks: {
|
|
44
|
-
async activate() {
|
|
45
|
-
// Called when a component is created or updated. Handles all the logic
|
|
46
|
-
// for starting and stopping watch notifications tied to the desired files.
|
|
47
|
-
|
|
48
|
-
const channelID = this.db.get("channelID") || uuid();
|
|
49
|
-
|
|
50
|
-
const startPageToken = await this.googleDrive.getPageToken();
|
|
51
|
-
const { expiration, resourceId } = await this.googleDrive.watchDrive(
|
|
52
|
-
channelID,
|
|
53
|
-
this.http.endpoint,
|
|
54
|
-
startPageToken,
|
|
55
|
-
this.drive === "myDrive" ? null : this.drive
|
|
56
|
-
);
|
|
57
|
-
// We use and increment the pageToken as new changes arrive, in run()
|
|
58
|
-
this.db.set("pageToken", startPageToken);
|
|
59
|
-
|
|
60
|
-
// Save metadata on the subscription so we can stop / renew later
|
|
61
|
-
// Subscriptions are tied to Google's resourceID, "an opaque value that
|
|
62
|
-
// identifies the watched resource". This value is included in request headers
|
|
63
|
-
this.db.set("subscription", { resourceId, expiration });
|
|
64
|
-
this.db.set("channelID", channelID);
|
|
65
|
-
},
|
|
66
|
-
async deactivate() {
|
|
67
|
-
const channelID = this.db.get("channelID");
|
|
68
|
-
const { resourceId } = this.db.get("subscription");
|
|
69
|
-
|
|
70
|
-
// Reset DB state before anything else
|
|
71
|
-
this.db.set("subscription", null);
|
|
72
|
-
this.db.set("channelID", null);
|
|
73
|
-
this.db.set("pageToken", null);
|
|
74
|
-
|
|
75
|
-
if (!channelID) {
|
|
76
|
-
console.log(
|
|
77
|
-
"Channel not found, cannot stop notifications for non-existent channel"
|
|
78
|
-
);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!resourceId) {
|
|
83
|
-
console.log(
|
|
84
|
-
"No resource ID found, cannot stop notifications for non-existent resource"
|
|
85
|
-
);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
await this.googleDrive.stopNotifications(channelID, resourceId);
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
async run(event) {
|
|
93
|
-
// This function is polymorphic: it can be triggered as a cron job, to make sure we renew
|
|
94
|
-
// watch requests for specific files, or via HTTP request (the change payloads from Google)
|
|
95
|
-
|
|
96
|
-
let subscription = this.db.get("subscription");
|
|
97
|
-
const channelID = this.db.get("channelID");
|
|
98
|
-
const pageToken = this.db.get("pageToken");
|
|
99
|
-
|
|
100
|
-
// Component was invoked by timer
|
|
101
|
-
if (event.interval_seconds) {
|
|
102
|
-
if (!subscription || !subscription.resourceId) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
console.log(
|
|
106
|
-
`Checking for resubscription on resource ${subscription.resourceId}`
|
|
107
|
-
);
|
|
108
|
-
// If the subscription for this resource will expire before the next run,
|
|
109
|
-
// stop the existing subscription and renew. Expiration is in ms.
|
|
110
|
-
if (
|
|
111
|
-
subscription.expiration <
|
|
112
|
-
+new Date() + event.interval_seconds * 1000
|
|
113
|
-
) {
|
|
114
|
-
console.log(
|
|
115
|
-
`Notifications for resource ${subscription.resourceId} are expiring at ${subscription.expiration}. Renewing`
|
|
116
|
-
);
|
|
117
|
-
await this.googleDrive.stopNotifications(
|
|
118
|
-
channelID,
|
|
119
|
-
subscription.resourceId
|
|
120
|
-
);
|
|
121
|
-
const { expiration, resourceId } = await this.googleDrive.watchDrive(
|
|
122
|
-
channelID,
|
|
123
|
-
this.http.endpoint,
|
|
124
|
-
pageToken,
|
|
125
|
-
this.drive === "myDrive" ? null : this.drive
|
|
126
|
-
);
|
|
127
|
-
this.db.set("subscription", { expiration, resourceId });
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const { headers } = event;
|
|
133
|
-
|
|
134
|
-
if (headers["x-goog-resource-state"] === "sync") {
|
|
135
|
-
console.log("Sync notification, exiting early");
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
!headers["x-goog-resource-state"] ||
|
|
141
|
-
!headers["x-goog-resource-id"] ||
|
|
142
|
-
!headers["x-goog-resource-uri"] ||
|
|
143
|
-
!headers["x-goog-message-number"]
|
|
144
|
-
) {
|
|
145
|
-
console.log("Request missing necessary headers: ", headers);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const incomingChannelID = headers["x-goog-channel-id"];
|
|
150
|
-
if (incomingChannelID !== channelID) {
|
|
151
|
-
console.log(
|
|
152
|
-
`Channel ID of ${incomingChannelID} not equal to deployed component channel of ${channelID}`
|
|
153
|
-
);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (headers["x-goog-resource-id"] !== subscription.resourceId) {
|
|
158
|
-
console.log(
|
|
159
|
-
`Resource ID of ${resourceId} not currently being tracked. Exiting`
|
|
160
|
-
);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!includes(this.updateTypes, headers["x-goog-resource-state"])) {
|
|
165
|
-
console.log(
|
|
166
|
-
`Update type ${headers["x-goog-resource-state"]} not in list of updates to watch: `,
|
|
167
|
-
this.updateTypes
|
|
168
|
-
);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// We observed false positives where a single change to a document would trigger two changes:
|
|
173
|
-
// one to "properties" and another to "content,properties". But changes to properties
|
|
174
|
-
// alone are legitimate, most users just won't want this source to emit in those cases.
|
|
175
|
-
// If x-goog-changed is _only_ set to "properties", only move on if the user set the prop
|
|
176
|
-
if (
|
|
177
|
-
!this.watchForPropertiesChanges &&
|
|
178
|
-
headers["x-goog-changed"] === "properties"
|
|
179
|
-
) {
|
|
180
|
-
console.log(
|
|
181
|
-
"Change to properties only, which this component is set to ignore. Exiting"
|
|
182
|
-
);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const {
|
|
187
|
-
changedFiles,
|
|
188
|
-
newStartPageToken,
|
|
189
|
-
} = await this.googleDrive.getChanges(
|
|
190
|
-
pageToken,
|
|
191
|
-
this.drive === "myDrive" ? null : this.drive
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
this.db.set("pageToken", newStartPageToken);
|
|
195
|
-
|
|
196
|
-
for (const file of changedFiles) {
|
|
197
|
-
console.log(file);
|
|
198
|
-
const eventToEmit = {
|
|
199
|
-
file,
|
|
200
|
-
change: {
|
|
201
|
-
state: headers["x-goog-resource-state"],
|
|
202
|
-
resourceURI: headers["x-goog-resource-uri"],
|
|
203
|
-
changed: headers["x-goog-changed"], // "Additional details about the changes. Possible values: content, parents, children, permissions"
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
this.$emit(eventToEmit, {
|
|
208
|
-
summary: file.name,
|
|
209
|
-
id: headers["x-goog-message-number"],
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
};
|