@pipedream/google_drive 1.12.2 → 2.0.0
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConfigurationError } from "@pipedream/platform";
|
|
1
2
|
import fs from "fs";
|
|
2
3
|
import stream from "stream";
|
|
3
4
|
import { promisify } from "util";
|
|
@@ -24,14 +25,15 @@ const SHORTCUT_MIME_TYPE = "application/vnd.google-apps.shortcut";
|
|
|
24
25
|
export default {
|
|
25
26
|
key: "google_drive-download-file",
|
|
26
27
|
name: "Download File",
|
|
27
|
-
description: "Download
|
|
28
|
+
description: "Download one or more files from Google Drive to the `/tmp` directory or return their contents as buffers."
|
|
29
|
+
+ " Select multiple files to download a batch in a single run."
|
|
28
30
|
+ " Use to fetch a file's contents for processing in downstream steps — e.g., parsing a CSV, extracting text from a PDF, or re-uploading to another service."
|
|
29
31
|
+ " For Google Workspace files (Docs, Sheets, Slides, Drawings, Apps Script), exports to an Office-compatible format by default:"
|
|
30
32
|
+ " Docs → `.docx`, Sheets → `.xlsx`, Slides → `.pptx`, Drawings → PNG, Apps Script → JSON."
|
|
31
33
|
+ " Pass `mimeType` to force a specific format. Shortcuts are resolved to their target automatically."
|
|
32
34
|
+ " Folders, Forms, and My Maps cannot be downloaded via this action."
|
|
33
35
|
+ " [See the documentation](https://developers.google.com/drive/api/v3/manage-downloads)",
|
|
34
|
-
version: "0.
|
|
36
|
+
version: "0.2.0",
|
|
35
37
|
annotations: {
|
|
36
38
|
destructiveHint: false,
|
|
37
39
|
openWorldHint: true,
|
|
@@ -48,6 +50,19 @@ export default {
|
|
|
48
50
|
description: "The shared drive the file is in, if any. Leave empty for files in My Drive.",
|
|
49
51
|
optional: true,
|
|
50
52
|
},
|
|
53
|
+
fileIds: {
|
|
54
|
+
propDefinition: [
|
|
55
|
+
googleDrive,
|
|
56
|
+
"fileId",
|
|
57
|
+
(c) => ({
|
|
58
|
+
drive: c.drive,
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
type: "string[]",
|
|
62
|
+
label: "Files",
|
|
63
|
+
description: "The Google Drive file(s) to download. Select one or more files to download a batch in a single run. Accepts file IDs (opaque Drive identifiers). Shortcuts are resolved to their target automatically.",
|
|
64
|
+
optional: true,
|
|
65
|
+
},
|
|
51
66
|
fileId: {
|
|
52
67
|
propDefinition: [
|
|
53
68
|
googleDrive,
|
|
@@ -56,7 +71,8 @@ export default {
|
|
|
56
71
|
drive: c.drive,
|
|
57
72
|
}),
|
|
58
73
|
],
|
|
59
|
-
description: "
|
|
74
|
+
description: "A single Google Drive file to download. Kept for backwards compatibility — prefer `Files` for new configurations. If both are set, this file is included alongside the ones in `Files`.",
|
|
75
|
+
optional: true,
|
|
60
76
|
},
|
|
61
77
|
filePath: {
|
|
62
78
|
type: "string",
|
|
@@ -82,7 +98,7 @@ export default {
|
|
|
82
98
|
`),
|
|
83
99
|
optional: true,
|
|
84
100
|
async options() {
|
|
85
|
-
const fileId = this.fileId;
|
|
101
|
+
const fileId = this.fileId ?? this.fileIds?.[0];
|
|
86
102
|
if (!fileId) {
|
|
87
103
|
return googleWorkspaceExportFormats;
|
|
88
104
|
}
|
|
@@ -127,104 +143,150 @@ export default {
|
|
|
127
143
|
},
|
|
128
144
|
},
|
|
129
145
|
async run({ $ }) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
resolvedFromShortcut = true;
|
|
144
|
-
fileMetadata = await this.googleDrive.getFile(targetId, {
|
|
145
|
-
fields: "name,mimeType,webViewLink",
|
|
146
|
-
});
|
|
147
|
-
}
|
|
146
|
+
// Combine the batch input (`fileIds`) with the legacy single `fileId`, keeping
|
|
147
|
+
// order and dropping duplicates so the same file isn't downloaded twice.
|
|
148
|
+
const requestedIds = [
|
|
149
|
+
...(this.fileIds ?? []),
|
|
150
|
+
...(this.fileId
|
|
151
|
+
? [
|
|
152
|
+
this.fileId,
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
];
|
|
156
|
+
const fileIds = [
|
|
157
|
+
...new Set(requestedIds),
|
|
158
|
+
];
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
);
|
|
160
|
+
if (fileIds.length === 0) {
|
|
161
|
+
throw new ConfigurationError("Select at least one file to download (`Files` or `File`).");
|
|
162
|
+
}
|
|
163
|
+
if (fileIds.length > 1 && this.filePath) {
|
|
164
|
+
throw new ConfigurationError("`Destination File Path` can only be used when downloading a single file. Remove it to download multiple files (each saves to `/tmp/<file name>`).");
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
const
|
|
167
|
+
const pipeline = promisify(stream.pipeline);
|
|
168
|
+
|
|
169
|
+
// Downloads a single file (resolving shortcuts + Workspace export formats),
|
|
170
|
+
// either writing it to /tmp or returning its contents as a buffer.
|
|
171
|
+
const downloadOne = async (requestedFileId) => {
|
|
172
|
+
let fileMetadata = await this.googleDrive.getFile(requestedFileId, {
|
|
173
|
+
fields: "name,mimeType,shortcutDetails,webViewLink",
|
|
174
|
+
});
|
|
158
175
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
// Shortcuts point at a target file; resolve and download the target instead.
|
|
177
|
+
let downloadFileId = requestedFileId;
|
|
178
|
+
let resolvedFromShortcut = false;
|
|
179
|
+
if (fileMetadata.mimeType === SHORTCUT_MIME_TYPE) {
|
|
180
|
+
const targetId = fileMetadata.shortcutDetails?.targetId;
|
|
181
|
+
if (!targetId) {
|
|
182
|
+
throw new Error(`Shortcut "${fileMetadata.name}" has no target file and cannot be downloaded.`);
|
|
183
|
+
}
|
|
184
|
+
downloadFileId = targetId;
|
|
185
|
+
resolvedFromShortcut = true;
|
|
186
|
+
fileMetadata = await this.googleDrive.getFile(targetId, {
|
|
187
|
+
fields: "name,mimeType,webViewLink",
|
|
188
|
+
});
|
|
166
189
|
}
|
|
167
|
-
|
|
190
|
+
|
|
191
|
+
const sourceMimeType = fileMetadata.mimeType;
|
|
192
|
+
|
|
193
|
+
if (unsupportedWorkspaceMimes[sourceMimeType]) {
|
|
168
194
|
throw new Error(
|
|
169
|
-
`
|
|
195
|
+
`Cannot download file of type "${sourceMimeType}": ${unsupportedWorkspaceMimes[sourceMimeType]}`,
|
|
170
196
|
);
|
|
171
197
|
}
|
|
172
|
-
}
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
const isWorkspaceDocument = sourceMimeType.includes(GOOGLE_DRIVE_MIME_TYPE_PREFIX);
|
|
200
|
+
|
|
201
|
+
// Fallback: user value -> static default -> runtime getExportFormats().
|
|
202
|
+
let effectiveMimeType = this.mimeType;
|
|
203
|
+
if (isWorkspaceDocument && !effectiveMimeType) {
|
|
204
|
+
effectiveMimeType = defaultExportMimeBySource[sourceMimeType];
|
|
205
|
+
if (!effectiveMimeType) {
|
|
206
|
+
const exportFormats = await this.googleDrive.getExportFormats();
|
|
207
|
+
effectiveMimeType = exportFormats[sourceMimeType]?.[0];
|
|
208
|
+
}
|
|
209
|
+
if (!effectiveMimeType) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`No export format available for "${sourceMimeType}". Set Conversion Format explicitly.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
182
215
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
// See https://developers.google.com/drive/api/v3/mime-types for Google MIME types.
|
|
217
|
+
const file = isWorkspaceDocument
|
|
218
|
+
? await this.googleDrive.downloadWorkspaceFile(downloadFileId, {
|
|
219
|
+
mimeType: effectiveMimeType,
|
|
220
|
+
})
|
|
221
|
+
: await this.googleDrive.getFile(downloadFileId, {
|
|
222
|
+
alt: "media",
|
|
223
|
+
});
|
|
186
224
|
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
if (this.getBufferResponse) {
|
|
226
|
+
const chunks = [];
|
|
227
|
+
for await (const chunk of file) {
|
|
228
|
+
chunks.push(chunk);
|
|
229
|
+
}
|
|
230
|
+
const buffer = Buffer.concat(chunks);
|
|
189
231
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
232
|
+
return {
|
|
233
|
+
fileId: requestedFileId,
|
|
234
|
+
fileMetadata,
|
|
235
|
+
webViewLink: fileMetadata.webViewLink,
|
|
236
|
+
content: buffer,
|
|
237
|
+
resolvedFromShortcut,
|
|
238
|
+
};
|
|
193
239
|
}
|
|
194
|
-
|
|
240
|
+
|
|
241
|
+
let filePath;
|
|
242
|
+
if (this.filePath) {
|
|
243
|
+
filePath = this.filePath.includes("tmp/")
|
|
244
|
+
? this.filePath
|
|
245
|
+
: `/tmp/${this.filePath}`;
|
|
246
|
+
} else {
|
|
247
|
+
let defaultName = fileMetadata.name;
|
|
248
|
+
if (isWorkspaceDocument) {
|
|
249
|
+
const ext = extensionByMime[effectiveMimeType];
|
|
250
|
+
if (ext && !defaultName.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {
|
|
251
|
+
defaultName = `${defaultName}.${ext}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
filePath = `/tmp/${defaultName}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await pipeline(file, fs.createWriteStream(filePath));
|
|
195
258
|
|
|
196
259
|
return {
|
|
197
|
-
fileId:
|
|
260
|
+
fileId: requestedFileId,
|
|
198
261
|
fileMetadata,
|
|
199
262
|
webViewLink: fileMetadata.webViewLink,
|
|
200
|
-
|
|
263
|
+
filePath,
|
|
264
|
+
resolvedFromShortcut,
|
|
201
265
|
};
|
|
202
|
-
}
|
|
266
|
+
};
|
|
203
267
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
filePath = `/tmp/${defaultName}`;
|
|
268
|
+
// Single-file downloads keep the original (non-array) return shape for
|
|
269
|
+
// backwards compatibility; batches return an array of per-file results.
|
|
270
|
+
if (fileIds.length === 1) {
|
|
271
|
+
const result = await downloadOne(fileIds[0]);
|
|
272
|
+
const verb = result.resolvedFromShortcut
|
|
273
|
+
? "Resolved shortcut and downloaded"
|
|
274
|
+
: "Successfully downloaded";
|
|
275
|
+
const what = this.getBufferResponse
|
|
276
|
+
? `raw content for file "${result.fileMetadata.name}"`
|
|
277
|
+
: `the file, "${result.fileMetadata.name}"`;
|
|
278
|
+
$.export("$summary", `${verb} ${what}`);
|
|
279
|
+
return result;
|
|
218
280
|
}
|
|
219
281
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
282
|
+
const files = [];
|
|
283
|
+
for (const id of fileIds) {
|
|
284
|
+
files.push(await downloadOne(id));
|
|
285
|
+
}
|
|
286
|
+
$.export("$summary", `Successfully downloaded ${files.length} file(s).`);
|
|
223
287
|
return {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
webViewLink: fileMetadata.webViewLink,
|
|
227
|
-
filePath,
|
|
288
|
+
files,
|
|
289
|
+
count: files.length,
|
|
228
290
|
};
|
|
229
291
|
},
|
|
230
292
|
};
|
|
@@ -8,8 +8,8 @@ import { PAGINATION_TOKEN_FIELD } from "../../common/constants.mjs";
|
|
|
8
8
|
export default {
|
|
9
9
|
key: "google_drive-list-files",
|
|
10
10
|
name: "List Files",
|
|
11
|
-
description: "List files from a specific folder. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/list) for more information",
|
|
12
|
-
version: "0.
|
|
11
|
+
description: "List files from a specific folder. Set `Max Results` to cap how many files are returned per run, then pass the returned `nextPageToken` back in as `Page Token` on the next run to page through a large folder in fixed-size batches. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/list) for more information",
|
|
12
|
+
version: "1.0.0",
|
|
13
13
|
annotations: {
|
|
14
14
|
destructiveHint: false,
|
|
15
15
|
openWorldHint: true,
|
|
@@ -55,7 +55,17 @@ export default {
|
|
|
55
55
|
description: "Filter by file name that contains a specific text",
|
|
56
56
|
type: "string",
|
|
57
57
|
optional: true,
|
|
58
|
-
|
|
58
|
+
},
|
|
59
|
+
filterType: {
|
|
60
|
+
type: "string",
|
|
61
|
+
label: "Filter Type",
|
|
62
|
+
description: "Whether to return files with names containing the Filter Text or files with names that match the Filter Text exactly. Defaults to \"CONTAINS\". Not relevant unless `Filter Text` is entered.",
|
|
63
|
+
options: [
|
|
64
|
+
"CONTAINS",
|
|
65
|
+
"EXACT MATCH",
|
|
66
|
+
],
|
|
67
|
+
default: "CONTAINS",
|
|
68
|
+
optional: true,
|
|
59
69
|
},
|
|
60
70
|
trashed: {
|
|
61
71
|
label: "Trashed",
|
|
@@ -63,22 +73,19 @@ export default {
|
|
|
63
73
|
description: "If `true`, list **only** trashed files. If `false`, list **only** non-trashed files. Keep it empty to include both.",
|
|
64
74
|
optional: true,
|
|
65
75
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return props;
|
|
76
|
+
maxResults: {
|
|
77
|
+
label: "Max Results",
|
|
78
|
+
type: "integer",
|
|
79
|
+
description: "The maximum number of files to return per run. Leave empty to return every file in the folder. Combine with `Page Token` to page through a large folder in fixed-size batches across multiple runs.",
|
|
80
|
+
optional: true,
|
|
81
|
+
min: 1,
|
|
82
|
+
},
|
|
83
|
+
pageToken: {
|
|
84
|
+
label: "Page Token",
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "A cursor for resuming a previous run. Pass the `nextPageToken` returned by an earlier run to continue listing from where it stopped instead of starting over. Leave empty to start from the beginning of the folder.",
|
|
87
|
+
optional: true,
|
|
88
|
+
},
|
|
82
89
|
},
|
|
83
90
|
async run({ $ }) {
|
|
84
91
|
if (this.limitToMyDrive && this.drive && !isMyDrive(this.drive)) {
|
|
@@ -112,16 +119,53 @@ export default {
|
|
|
112
119
|
}
|
|
113
120
|
opts.fields = fieldsValue;
|
|
114
121
|
}
|
|
122
|
+
|
|
123
|
+
const maxResults = this.maxResults === undefined
|
|
124
|
+
? undefined
|
|
125
|
+
: Number(this.maxResults);
|
|
126
|
+
if (maxResults !== undefined && (!Number.isInteger(maxResults) || maxResults < 1)) {
|
|
127
|
+
throw new ConfigurationError("`Max Results` must be a positive integer.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const startToken = this.pageToken;
|
|
115
131
|
const allFiles = [];
|
|
116
|
-
let pageToken;
|
|
132
|
+
let pageToken = startToken;
|
|
117
133
|
do {
|
|
134
|
+
const pageOpts = {
|
|
135
|
+
...opts,
|
|
136
|
+
};
|
|
137
|
+
if (maxResults) {
|
|
138
|
+
// Request exactly as many as we still need (Drive caps pageSize at 1000).
|
|
139
|
+
// Aligning pageSize to the remaining count means the returned
|
|
140
|
+
// nextPageToken points right after the last file we keep, so no files
|
|
141
|
+
// are skipped when resuming — we never over-fetch and discard.
|
|
142
|
+
pageOpts.pageSize = Math.min(maxResults - allFiles.length, 1000);
|
|
143
|
+
}
|
|
118
144
|
const {
|
|
119
|
-
files, nextPageToken,
|
|
120
|
-
}
|
|
145
|
+
files = [], nextPageToken,
|
|
146
|
+
} = await this.googleDrive.listFilesInPage(pageToken, pageOpts);
|
|
121
147
|
allFiles.push(...files);
|
|
122
148
|
pageToken = nextPageToken;
|
|
123
|
-
} while (pageToken);
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
} while (pageToken && (!maxResults || allFiles.length < maxResults));
|
|
150
|
+
|
|
151
|
+
const isComplete = !pageToken;
|
|
152
|
+
const resumed = Boolean(startToken);
|
|
153
|
+
let summary = `${resumed
|
|
154
|
+
? "Resumed and returned"
|
|
155
|
+
: "Successfully found"} ${allFiles.length} file(s).`;
|
|
156
|
+
// Only surface batch state when the caller is actually paging.
|
|
157
|
+
if (maxResults || startToken || !isComplete) {
|
|
158
|
+
summary += isComplete
|
|
159
|
+
? " Reached the end of the folder."
|
|
160
|
+
: " More results remain — pass `nextPageToken` as `Page Token` on the next run to continue.";
|
|
161
|
+
}
|
|
162
|
+
$.export("$summary", summary);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
files: allFiles,
|
|
166
|
+
count: allFiles.length,
|
|
167
|
+
nextPageToken: pageToken ?? null,
|
|
168
|
+
isComplete,
|
|
169
|
+
};
|
|
126
170
|
},
|
|
127
171
|
};
|