1const { apifyGoogleAuth } = require('apify-google-auth');
2const { google } = require('googleapis');
3const { OPERATIONS_TYPES } = require('./consts');
4const { UploadOperation, DeleteFolderOperation } = require('./operations/index');
5const { Folder } = require('./operations/helper');
6
7const DRIVE_ERROR_MESSAGES = {
8 insufficientPermissions: 'The user does not have sufficient permissions for this file',
9};
10class Service {
11
12
13
14 constructor(config) {
15 this.config = config;
16 }
17
18 async init() {
19 console.log('Initializing drive service...');
20
21 const { tokensStore, googleApisCredentials } = this.config;
22
23 this._auth = await apifyGoogleAuth({
24 scope: 'drive',
25 tokensStore,
26 credentials: googleApisCredentials,
27 });
28
29
30
31
32
33 this._drive = google.drive({ version: 'v3', auth: this._auth });
34 }
35
36
37
38
39
40 async listFiles(params = {}) {
41 const defaults = {
42 preListLogMsgFunction: ({ page, enableLog = false }) => enableLog && console.log(`Getting files (page ${page})...`),
43 afterListLogMsgFunction: ({ files, enableLog = false }) => enableLog && console.log(`We found ${files.length} files`),
44 };
45 const {
46 extraQ,
47 pageSize = 1000,
48 token = '',
49 fields = 'nextPageToken, files(*)',
50 root = false,
51 trashed = false,
52 spaces = 'drive',
53 enableLog = false,
54 filesList = [],
55 page = 1,
56 preListLogMsgFunction = defaults.preListLogMsgFunction,
57 afterListLogMsgFunction = defaults.afterListLogMsgFunction,
58 } = params;
59
60 preListLogMsgFunction({ page, enableLog });
61
62 const q = this.buildQuery({
63 root,
64 trashed,
65 extraQ,
66 });
67 const { data } = await this._drive.files.list(
68 {
69 q,
70 spaces,
71 pageToken: token,
72 pageSize,
73 fields,
74 },
75 );
76 Array.prototype.push.apply(filesList, data.files);
77 const files = data.nextPageToken
78 ? await this.listFiles({
79 extraQ,
80 pageSize,
81 token,
82 fields,
83 root,
84 trashed,
85 spaces,
86 filesList,
87 page: page + 1,
88 })
89 : filesList;
90
91 if (filesList.length === data.files.length) {
92 afterListLogMsgFunction({ files, enableLog });
93 }
94 return files;
95 }
96
97 createFile(params) {
98 const {
99 resource,
100 media,
101 fields = '*',
102 } = params;
103
104 return this._drive.files.create({
105 resource,
106 media,
107 fields,
108 });
109 }
110
111 updateFile(params) {
112 const {
113 fileId,
114 media,
115 fields = '*',
116 } = params;
117
118 return this._drive.files.update({
119 fileId,
120 media,
121 fields,
122 });
123 }
124
125
126
127
128 async listFilesWithoutFolders(params = {}) {
129 let { extraQ = '' } = params;
130 if (extraQ === '') extraQ = 'mimeType != \'application/vnd.google-apps.folder\'';
131
132 if (!extraQ.includes('mimeType')) extraQ += ' and mimeType != \'application/vnd.google-apps.folder\'';
133 return this.listFiles({
134 ...params,
135 extraQ,
136 });
137 }
138
139
140
141
142 async listFolders(params = {}) {
143 const defaults = {
144 preListLogMsgFunction: ({ page, enableLog = false }) => enableLog && console.log(`Getting folders (page ${page})...`),
145 afterListLogMsgFunction: ({ folders, enableLog = false }) => enableLog && console.log(`We found ${folders.length} folders`),
146 };
147 const {
148 extraQ,
149 pageSize = 1000,
150 token = '',
151 fields = 'nextPageToken, files(id, name, parents)',
152 root = false,
153 trashed = false,
154 spaces = 'drive',
155 enableLog = false,
156 folderList = [],
157 page = 1,
158 preListLogMsgFunction = defaults.preListLogMsgFunction,
159 afterListLogMsgFunction = defaults.afterListLogMsgFunction,
160 } = params;
161 const mimeType = 'application/vnd.google-apps.folder';
162
163 preListLogMsgFunction({ page, enableLog });
164
165 const q = this.buildQuery({
166 mimeType,
167 root,
168 trashed,
169 extraQ,
170 });
171 const { data } = await this._drive.files.list(
172 {
173 q,
174 spaces,
175 pageToken: token,
176 pageSize,
177 fields,
178 },
179 );
180 Array.prototype.push.apply(folderList, data.files);
181 const folders = data.nextPageToken
182 ? await this.listFolders({
183 extraQ,
184 pageSize,
185 token,
186 fields,
187 root,
188 trashed,
189 spaces,
190 folderList,
191 page: page + 1,
192 })
193 : folderList;
194
195 if (folderList.length === data.files.length) {
196 afterListLogMsgFunction({ folders, enableLog });
197 }
198 return folders;
199 }
200
201
202
203
204 async listRootFolders(params = {}) {
205 const {
206 extraQ,
207 pageSize = 1000,
208 token = '',
209 fields = 'nextPageToken, files(id, name, parents)',
210 trashed = false,
211 } = params;
212
213 console.log('Getting root folders...');
214
215 const rootFolders = await this.listFolders({
216 extraQ,
217 pageSize,
218 token,
219 fields,
220 root: true,
221 trashed,
222 });
223 console.log(`We found ${rootFolders.length} root folders.`);
224 return rootFolders;
225 }
226
227 buildQuery({ mimeType, root, trashed, extraQ }) {
228 const qArr = [];
229 if (mimeType) qArr.push(`mimeType='${mimeType}'`);
230 if (root) qArr.push('\'root\' in parents');
231 if (typeof trashed === 'boolean') qArr.push(`trashed=${trashed}`);
232 if (extraQ) qArr.push(`${extraQ}`);
233 return qArr.join(' and ');
234 }
235
236 async createOrUpdateFile(params) {
237 const searchFiles = await this.getFileData({ ...params });
238
239 if (searchFiles.length > 0) return this.updateFile({ fileId: searchFiles[0].id, ...params });
240
241 return this.createFile(params);
242 }
243
244 async getFileData(params) {
245 const {
246 resource: { name, parents },
247 } = params;
248
249 let extraQ = `name = '${name}'`;
250 if (parents && parents.length > 0) {
251 extraQ += ` and (${parents.map(p => `'${p}' in parents`)
252 .join(' or ')})`;
253 }
254 return this.listFilesWithoutFolders({
255 ...params,
256 extraQ,
257 });
258 }
259
260 async uploadFile(file, parentFolderId, filesProvider) {
261 if (typeof file !== 'object') {
262 throw new Error(`DriveService.copyFile(): Parameter "file" must be of type object, provided value was "${file}"`);
263 }
264 const name = filesProvider.getFileName(file.key);
265 console.log(`Copying file ${name}...`);
266
267 const params = {
268 resource: {
269 ...file.options.resource,
270 name,
271 parents: [parentFolderId],
272 },
273 media: {
274 ...file.options.media,
275 body: await filesProvider.getFileStream(file.key),
276 },
277 };
278 return this.createOrUpdateFile(params);
279 }
280
281 async execute() {
282 console.log('Executing operations...');
283 const { operations } = this.config;
284 let operationToExecute;
285 for (const operation of operations) {
286 const { type } = operation;
287 switch (type) {
288 case OPERATIONS_TYPES.UPLOAD: {
289 const { source, destination: inputDestination } = operation;
290 const destination = new Folder(inputDestination);
291 operationToExecute = new UploadOperation({ source, destination });
292 break;
293 }
294 case OPERATIONS_TYPES.FOLDERS_DELETE: {
295 const { folder: opFolder } = operation;
296 const folder = new Folder(opFolder);
297 operationToExecute = new DeleteFolderOperation({ folder });
298 break;
299 }
300
301 default: {
302 throw new Error(`DriveService.execute(): Unknown operation type "${operation}"!`);
303 }
304 }
305 await operationToExecute.execute(this);
306 }
307 }
308
309
310
311
312
313
314 async getFolderInfo(folder) {
315 console.log(`Getting folder info ${folder}...`);
316
317 this.checkFolderOrThrow(folder);
318
319 const result = { folderId: null };
320
321 const folders = folder.getFolders();
322
323 let currentFolder;
324
325 for (const folderEl of folders) {
326 let searchedFolder;
327 if (folderEl.root) {
328 const searchFolders = await this.listFolders({
329 extraQ: `name='${folderEl.name}'`,
330 });
331 searchedFolder = searchFolders.find((f) => {
332 if (folderEl.id) return f.id === folderEl.id;
333 return f.name === folderEl.name;
334 });
335 } else {
336 const searchFolders = await this.listFolders({
337 extraQ: `name = '${folderEl.name}' and '${currentFolder.id}' in parents`,
338 });
339
340
341 const { name } = folderEl;
342 const { id } = currentFolder;
343 searchedFolder = searchFolders.find(f => f.name === name && f.parents[0] === id);
344 }
345 if (!searchedFolder) {
346 result.folderId = null;
347 break;
348 }
349 currentFolder = searchedFolder;
350 result.folderId = currentFolder.id;
351 }
352 return result;
353 }
354
355 async deleteFolder(folderId) {
356 if (!folderId) throw new Error('Parameter "folderId" is not defined!');
357
358 console.log(`Deleting folder with id ${folderId}...`);
359
360 try {
361 const result = await this._drive.files.delete({
362 fileId: folderId,
363 });
364 if (result.code === 404 && result.message.includes('File not found')) {
365 console.log(`Couldn't delete folder with id "${folderId}" because it doesn't exist`);
366 }
367 } catch (e) {
368 if (e.message.includes(DRIVE_ERROR_MESSAGES.insufficientPermissions)) {
369 throw new Error(`${DRIVE_ERROR_MESSAGES.insufficientPermissions} (id="${folderId}")`);
370 }
371 throw e;
372 }
373 }
374
375
376
377
378
379
380 async createFolder(folder) {
381 this.checkFolderOrThrow(folder);
382
383 console.log(`Creating folder ${folder}...`);
384
385 const result = { folderId: null };
386
387 const folders = folder.getFolders();
388
389 let currentFolder;
390
391 for (const folderEl of folders) {
392 const params = {
393 resource: {
394 name: folderEl.name,
395 mimeType: 'application/vnd.google-apps.folder',
396 } };
397 let searchedFolder;
398 if (folderEl.root) {
399 const searchFolders = await this.listFolders({
400 root: !folderEl.id,
401 extraQ: `name='${folderEl.name}'`,
402 });
403 searchedFolder = searchFolders.find((f) => {
404 if (folderEl.id) return f.id === folderEl.id;
405 return f.name === folderEl.name;
406 });
407 } else {
408 const searchFolders = await this.listFolders({
409 extraQ: `name = '${folderEl.name}' and '${currentFolder.id}' in parents` });
410
411 const { name } = folderEl;
412 const { id } = currentFolder;
413 searchedFolder = searchFolders.find(f => f.name === name && f.parents[0] === id);
414 }
415 if (!searchedFolder) {
416 if (currentFolder) params.resource.parents = [currentFolder.id];
417 ({ data: currentFolder } = await this.createFile(params));
418 } else {
419 currentFolder = searchedFolder;
420 }
421
422 result.folderId = currentFolder.id;
423 }
424
425 console.log(`Folder created: folder="${folder}"\nid="${result.folderId}"`);
426
427 return result;
428 }
429
430 checkFolderOrThrow(folder) {
431 if (!folder || !(folder instanceof Folder)) {
432 throw new Error(`Parameter "folder" must be an instance of Folder! provided value was ${JSON.stringify(folder)}`);
433 }
434 }
435}
436
437module.exports = Service;