UNPKG

@pipedream/google_drive

Version:

Pipedream Google_drive Components

213 lines (199 loc) 6.85 kB
import includes from "lodash/includes.js"; import { v4 as uuid } from "uuid"; import googleDrive from "../google_drive.app.mjs"; import { WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS } from "../common/constants.mjs"; import { getListFilesOpts } from "../common/utils.mjs"; import commonDedupeChanges from "./common-dedupe-changes.mjs"; export default { props: { googleDrive, db: "$.service.db", http: { type: "$.interface.http", customResponse: true, }, drive: { propDefinition: [ googleDrive, "watchedDrive", ], description: "Defaults to My Drive. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", optional: false, }, timer: { label: "Push notification renewal schedule", description: "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**.", type: "$.interface.timer", static: { intervalSeconds: WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, }, hidden: true, }, }, hooks: { async activate() { // Called when a component is created or updated. Handles all the logic // for starting and stopping watch notifications tied to the desired // files. const channelID = uuid(); const { startPageToken, expiration, resourceId, } = await this.googleDrive.activateHook( channelID, this.http.endpoint, this.getDriveId(), ); // We use and increment the pageToken as new changes arrive, in run() this._setPageToken(startPageToken); // Save metadata on the subscription so we can stop / renew later // Subscriptions are tied to Google's resourceID, "an opaque value that // identifies the watched resource". This value is included in request headers this._setSubscription({ resourceId, expiration, }); this._setChannelID(channelID); }, async deactivate() { const channelID = this._getChannelID(); const { resourceId } = this._getSubscription(); await this.googleDrive.deactivateHook(channelID, resourceId); this._setSubscription(null); this._setChannelID(null); this._setPageToken(null); }, }, methods: { ...commonDedupeChanges.methods, _getSubscription() { return this.db.get("subscription"); }, _setSubscription(subscription) { this.db.set("subscription", subscription); }, _getChannelID() { return this.db.get("channelID"); }, _setChannelID(channelID) { this.db.set("channelID", channelID); }, _getPageToken() { return this.db.get("pageToken"); }, _setPageToken(pageToken) { this.db.set("pageToken", pageToken); }, isMyDrive(drive = this.drive) { return googleDrive.methods.isMyDrive(drive); }, getDriveId(drive = this.drive) { return googleDrive.methods.getDriveId(drive); }, getListFilesOpts(args = {}) { return getListFilesOpts(this.drive, { q: "mimeType != 'application/vnd.google-apps.folder' and trashed = false", ...args, }); }, /** * This method returns the types of updates/events from Google Drive that * the event source should listen to. This base implementation returns an * empty list, which means that any event source that extends this module * and that does not refine this implementation will essentially ignore * every incoming event from Google Drive. * * @returns * @type {UpdateType[]} */ getUpdateTypes() { return []; }, /** * This method is responsible for processing a list of changed files * according to the event source's purpose. As an abstract method, it must * be implemented by every event source that extends this module. * * @param {object[]} [changedFiles] - the list of file changes, as [defined * by the API](https://bit.ly/3h7WeUa) * @param {object} [headers] - an object containing the request headers of * the webhook call made by Google Drive */ processChanges() { throw new Error("processChanges is not implemented"); }, }, async run(event) { // This function is polymorphic: it can be triggered as a cron job, to make // sure we renew watch requests for specific files, or via HTTP request (the // change payloads from Google) const subscription = this._getSubscription(); const channelID = this._getChannelID(); const pageToken = this._getPageToken(); // Component was invoked by timer if (event.timestamp) { const { newChannelID, newPageToken, expiration, resourceId, } = await this.googleDrive.renewSubscription( this.drive, subscription, this.http.endpoint, channelID, pageToken, ); this._setSubscription({ expiration, resourceId, }); this._setChannelID(newChannelID); this._setPageToken(newPageToken); return; } else { this.http.respond({ status: 200, }); } const { headers } = event; if (!this.googleDrive.checkHeaders(headers, subscription, channelID)) { return; } if (!includes(this.getUpdateTypes(), headers["x-goog-resource-state"])) { console.log( `Update type ${headers["x-goog-resource-state"]} not in list of updates to watch: `, this.getUpdateTypes(), ); return; } // We observed false positives where a single change to a document would trigger two changes: // one to "properties" and another to "content,properties". But changes to properties // alone are legitimate, most users just won't want this source to emit in those cases. // If x-goog-changed is _only_ set to "properties", only move on if the user set the prop if ( !this.watchForPropertiesChanges && headers["x-goog-changed"] === "properties" ) { console.log( "Change to properties only, which this component is set to ignore. Exiting", ); return; } const driveId = this.getDriveId(); const changedFilesStream = this.googleDrive.listChanges(pageToken, driveId); for await (const changedFilesPage of changedFilesStream) { const { changedFiles, nextPageToken, } = changedFilesPage; // Process all the changed files retrieved from the current page await this.processChanges(changedFiles, headers); // After successfully processing the changed files, we store the page // token of the next page this._setPageToken(nextPageToken); } }, };