UNPKG

gsweet

Version:

Help with writing scripts to run against gSuite

273 lines (243 loc) 9.13 kB
// @ts-check /** @module */ /** * @file Handles talking to the Google Spreadsheet API * ## Links to Google Drive documentation * [mime-types](https://developers.google.com/drive/api/v3/mime-types) * [all file meta data](https://developers.google.com/drive/api/v3/reference/files) * [search parameters](https://developers.google.com/drive/api/v3/search-parameters) * a few meta types we aren't using that might be interesting are `starred, shared, description` * * NOTE: Before using init() **MUST** be called and a driveService passed in. * @author Tod Gentille <tod-gentille@pluralsight.com> * @license GPL-3.0-or-later [Full Text](https://spdx.org/licenses/GPL-3.0-or-later.html) */ const ds = require('./driveService') const logger = require('../utils/logger') const MAX_FILES_PER_PAGE = 1000 let _driveService /** Enum for the currently supported google mime-types */ const mimeType = { /** @type any */ /** @type {number} */ FOLDER: 1, /** FILE is not a google recognized value, used to mean anything other than a folder. */ FILE: 2, /** @type {number} */ SPREADSHEET: 3, /** @type {number} */ DOC: 4, /** @type {number} */ /** place to store the actual strings that google uses for mime-types */ properties: { /** @type object */ 1: {type: 'application/vnd.google-apps.folder'}, 2: {type: 'N/A'}, 3: {type: 'application/vnd.google-apps.spreadsheet'}, 4: {type: 'application/vnd.google-apps.document'}, }, /** Convenience function to return the google mime-types- called with our ENUM value */ getType(value) {return mimeType.properties[value].type}, } const FILE_META_FOR_NAME_SEARCH = 'files(id, name)' const FILE_META_FOR_FOLDER_SEARCH = 'files(id, name, mimeType)' /** Allow access to google drive APIs via the driveService (this version for testing) */ const init = (driveService) => { _driveService = driveService } /** In production just call this to set up access to the drive APIs */ const autoInit = () => { _driveService = ds.init() } /** * @typedef {object} WithNameExactMatch * @property {string} withName * @property {boolean} exactMatch */ /** * Get a list of files/folders that match * @param {WithNameExactMatch} fileOptions * @returns {Promise<Array.<{id:String,name:String}>>} * @example getFiles({withName:"someName", exactMatch:true}) */ const getFiles = async (fileOptions) => { const {withName} = fileOptions const {exactMatch} = fileOptions // NOTE: The filename has to be quoted const query = `name ${exactMatch ? ' = ' : 'contains'} '${withName}'` const response = await _driveService.files.list( { q: query, pageSize: MAX_FILES_PER_PAGE, fields: `nextPageToken, ${FILE_META_FOR_NAME_SEARCH}`, }) .catch(googleError => { const {errors} = googleError.response.data.error const errMsg = JSON.stringify(errors[0], null, 2) // logger.error(errMsg) throw (`For ${withName} - The Google Drive API returned:${errMsg}`) }) const {files} = response.data return files } /** * @typedef {object} WithNameOnly * @property {string} withName */ /** * Get a single file for the passed name. If a single file isn't found an error is thrown. // @ts-ignore * @param {WithNameOnly} withName * @returns {Promise<{id:String,name:String}>} a single object that has the FILE_META_FOR_NAME_SEARCH properties * @example getFile({withName:"someName"}) //forces exactMatch:true */ const getFile = async ({withName}) => { const files = await getFiles({withName, exactMatch: true}) if (files.length !== 1) { throw (`Found ${files.length} files.`) } return files[0] } /** * Convenience function that returns the id for a file * @param {WithNameExactMatch} withNameObj * @returns {Promise<string>} google id for the file * @example getFileId({withName:"SomeName"}) * */ const getFileId = async (withNameObj) => { const file = await getFile(withNameObj) return file.id } /** * Just get the files for the user. Will only return the google API max * of 1000 files. * @returns {Promise<Array.<{FILE_META_FOR_FOLDER_SEARCH}>>} array of objects, where each object * has the properties specified by the constant `FILE_META_FOR_FOLDER_SEARCH` * @example listFiles() * */ const listFiles = async () => { const response = await _driveService.files.list({ fields: `${FILE_META_FOR_FOLDER_SEARCH}`, pageSize: MAX_FILES_PER_PAGE, }) .catch(error => {throw (error)}) return response.data.files } /** * @typedef {object} FolderOptions * @property {string} withFolderId * @property {any} ofType */ /** * Example of how to use the nextPageToken to get all the files * in a folder when there are more than 1000 */ // const countAllFiles = async () => { // let nextPage = null // start on first page // let totalCount = 0 // do { // const response = await _driveService.files.list({ // pageToken: nextPage, // fields: `nextPageToken, ${FILE_META_FOR_FOLDER_SEARCH}`, // pageSize: MAX_FILES_PER_PAGE, // }) // .catch(error => { // logger.error(JSON.stringify(error)) // }) // nextPage = response.data.nextPageToken // if (response.data.files !== undefined) { // totalCount += response.data.files.length // } // logger.debug(nextPage) // } while (nextPage !== undefined && totalCount < 20000) // logger.info(`Total file count: ${totalCount}`) // } /** * Get all the Files in the passed folderId (ofType is optional) * @param {FolderOptions} folderOptions * @returns {Promise<Array<{name:string, id:string, mimeType:string}>>} array of file objects where each object has the properties * specified by the constant `FILE_META_FOR_FOLDER_SEARCH` * @example getFilesInFolder({withFolderId:"someId", ofType:mimeType:SPREADSHEET}) */ const getFilesInFolder = async (folderOptions) => { const {withFolderId} = folderOptions const {ofType} = folderOptions const mimeClause = getMimeTypeClause(ofType) const response = await _driveService.files.list( { q: `parents in '${withFolderId}' ${mimeClause}`, pageSize: MAX_FILES_PER_PAGE, fields: `nextPageToken, ${FILE_META_FOR_FOLDER_SEARCH}`, }) .catch(error => { const errMsg = JSON.stringify(error, null, 2) throw (`\r\nFor parent folder ${withFolderId} - files.list() returned:${errMsg}`) }) const nextToken = response.data.nextPageToken if (nextToken !== undefined) { logger.debug(`NEXT PAGE TOKEN ${response.data.nextPageToken}`) } const {files} = response.data return files } /** * Get just the names of the files in the folder (ofType is optional) * @param {FolderOptions} folderOptions * @returns {Promise<Array.<string>>} array of strings containing filenames * @example getFileNamesInFolder({withFolderId:"someId", ofType:mimeType.SPREADSHEET) */ const getFileNamesInFolder = async (folderOptions) => { const files = await getFilesInFolder(folderOptions) return files.map(e => e.name) } /** * Get the files in the parent folder and all the children folders (ofType is optional) * @param {FolderOptions} folderOptions * @returns {Promise<Array.<{FILE_META_FOR_FOLDER_SEARCH}>>} array of file objects where each object has the properties * specified by the constant `FILE_META_FOR_FOLDER_SEARCH` * @example getFilesRecursively({withFolderId:"someId", ofType:mimeType.SPREADSHEET}) */ const getFilesRecursively = async (folderOptions) => { let result = [] const {ofType} = folderOptions const {withFolderId} = folderOptions const folderType = mimeType.getType(mimeType.FOLDER) const allTypes = {withFolderId, ofType: undefined} const files = await getFilesInFolder(allTypes) for (const entry of files) { if (entry.mimeType === folderType) { const subFolderFiles = await getFilesRecursively({withFolderId: entry.id, ofType}) result = result.concat(subFolderFiles) } else { if ((ofType === undefined) || (entry.mimeType === mimeType.getType(ofType))) { result.push(entry) } } } return result } /** * Private helper function to look up the mimetype string for the passed enum and construct and "and" clause that * can be used in the API search query. The FILE enum isn't a type the API understands * but we use it to mean any type of file but NOT a folder. * @param {number} type * @returns {string} the additional clause to limit the search for the specified type. * For example if mimeType.SPREADSHEET was passed in, then the clause * will be returned. * @example getMimeTypeClause(mimeType.SPREADSHEET) will return `and mimeType = application/vnd.google-apps.spreadsheet` */ const getMimeTypeClause = (type) => { if (type === undefined) { return '' } if (type === mimeType.FILE) { return `and mimeType != '${mimeType.getType(mimeType.FOLDER)}'` } return `and mimeType = '${mimeType.getType(type)}'` } module.exports = { autoInit, init, getFile, getFiles, getFileId, listFiles, getFilesInFolder, getFileNamesInFolder, getFilesRecursively, mimeType, }