├── .github └── workflows │ ├── publishToNpm.yaml │ └── testing.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── ts-google-drive.iml └── vcs.xml ├── .npmignore ├── .sample.ts ├── LICENSE ├── README.md ├── icon.png ├── package.json ├── src ├── AuthClientBase.ts ├── File.ts ├── Query.ts ├── TsGoogleDrive.ts ├── index.ts └── types.ts ├── tests └── general.test.ts ├── tsconfig.json └── tslint.json /.github/workflows/publishToNpm.yaml: -------------------------------------------------------------------------------- 1 | name: Publish To NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 9 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 18 19 | registry-url: https://registry.npmjs.org/ 20 | 21 | # we run with npm install instead of npm ci to make sure the package can run with latest packages 22 | - name: npm install build and test 23 | run: | 24 | npm install 25 | npm run build --if-present 26 | npm run test:coverage 27 | env: 28 | CLIENT_EMAIL: ${{secrets.CLIENT_EMAIL }} 29 | PRIVATE_KEY: ${{secrets.PRIVATE_KEY }} 30 | FOLDER_ID: ${{secrets.FOLDER_ID }} 31 | 32 | - name: Upload code coverage 33 | if: env.CODECOV_TOKEN != '' 34 | run: | 35 | npm install codecov@latest -g 36 | mv coverage/coverage-final.json coverage/coverage.json 37 | codecov 38 | 39 | - name: Publish to NPM 40 | run: | 41 | if [ "$NPM_TOKEN" != "" ]; then 42 | npm publish 43 | else 44 | echo "Skip Publish. NPM_TOKEN not exists." 45 | exit 1 46 | fi 47 | env: 48 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 49 | 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | testing: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | max-parallel: 1 14 | matrix: 15 | node-version: [14.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | 20 | - name: Cache node modules 21 | uses: actions/cache@v1 22 | with: 23 | path: node_modules 24 | key: npm-${{ hashFiles('package.json') }}-${{matrix.node-version}} 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: npm install, build and test 32 | run: | 33 | npm install 34 | npm run build 35 | npm run test 36 | env: 37 | CLIENT_EMAIL: ${{secrets.CLIENT_EMAIL }} 38 | PRIVATE_KEY: ${{secrets.PRIVATE_KEY }} 39 | FOLDER_ID: ${{secrets.FOLDER_ID }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | # Customize 4 | package-lock.json 5 | credential.json 6 | build 7 | .env 8 | 9 | ### JetBrains template 10 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 11 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 12 | 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/dictionaries 18 | .idea/**/shelf 19 | 20 | # Generated files 21 | .idea/**/contentModel.xml 22 | 23 | # Sensitive or high-churn files 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | .idea/**/dbnavigator.xml 31 | 32 | # Gradle 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # Gradle and Maven with auto-import 37 | # When using Gradle or Maven with auto-import, you should exclude module files, 38 | # since they will be recreated, and may cause churn. Uncomment if using 39 | # auto-import. 40 | # .idea/artifacts 41 | # .idea/compiler.xml 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | # *.iml 46 | # *.ipr 47 | 48 | # CMake 49 | cmake-build-*/ 50 | 51 | # Mongo Explorer plugin 52 | .idea/**/mongoSettings.xml 53 | 54 | # File-based project format 55 | *.iws 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Editor-based Rest Client 76 | .idea/httpRequests 77 | 78 | # Android studio 3.1+ serialized cache file 79 | .idea/caches/build_file_checksums.ser 80 | 81 | ### Node template 82 | # Logs 83 | logs 84 | *.log 85 | npm-debug.log* 86 | yarn-debug.log* 87 | yarn-error.log* 88 | lerna-debug.log* 89 | 90 | # Diagnostic reports (https://nodejs.org/api/report.html) 91 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 92 | 93 | # Runtime data 94 | pids 95 | *.pid 96 | *.seed 97 | *.pid.lock 98 | 99 | # Directory for instrumented libs generated by jscoverage/JSCover 100 | lib-cov 101 | 102 | # Coverage directory used by tools like istanbul 103 | coverage 104 | *.lcov 105 | 106 | # nyc test coverage 107 | .nyc_output 108 | 109 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 110 | .grunt 111 | 112 | # Bower dependency directory (https://bower.io/) 113 | bower_components 114 | 115 | # node-waf configuration 116 | .lock-wscript 117 | 118 | # Compiled binary addons (https://nodejs.org/api/addons.html) 119 | build/Release 120 | 121 | # Dependency directories 122 | node_modules/ 123 | jspm_packages/ 124 | 125 | # TypeScript v1 declaration files 126 | typings/ 127 | 128 | # TypeScript cache 129 | *.tsbuildinfo 130 | 131 | # Optional npm cache directory 132 | .npm 133 | 134 | # Optional eslint cache 135 | .eslintcache 136 | 137 | # Microbundle cache 138 | .rpt2_cache/ 139 | .rts2_cache_cjs/ 140 | .rts2_cache_es/ 141 | .rts2_cache_umd/ 142 | 143 | # Optional REPL history 144 | .node_repl_history 145 | 146 | # Output of 'npm pack' 147 | *.tgz 148 | 149 | # Yarn Integrity file 150 | .yarn-integrity 151 | 152 | # dotenv environment variables file 153 | .env 154 | .env.test 155 | 156 | # parcel-bundler cache (https://parceljs.org/) 157 | .cache 158 | 159 | # Next.js build output 160 | .next 161 | 162 | # Nuxt.js build / generate output 163 | .nuxt 164 | dist 165 | 166 | # Gatsby files 167 | .cache/ 168 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 169 | # https://nextjs.org/blog/next-9-1#public-directory-support 170 | # public 171 | 172 | # vuepress build output 173 | .vuepress/dist 174 | 175 | # Serverless directories 176 | .serverless/ 177 | 178 | # FuseBox cache 179 | .fusebox/ 180 | 181 | # DynamoDB Local files 182 | .dynamodb/ 183 | 184 | # TernJS port file 185 | .tern-port 186 | 187 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/ts-google-drive.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | .github 4 | .nyc_output 5 | coverage 6 | src 7 | tests 8 | tslint.json 9 | icon.png 10 | -------------------------------------------------------------------------------- /.sample.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import {google} from "googleapis"; 3 | import {TsGoogleDrive} from "./src"; 4 | 5 | const tsGoogleDrive = new TsGoogleDrive({keyFilename: "serviceAccount.json"}); 6 | 7 | async function auth() { 8 | const drive1 = new TsGoogleDrive({keyFilename: "serviceAccount.json"}); 9 | const drive2 = new TsGoogleDrive({credentials: {client_email: "", private_key: ""}}); 10 | 11 | // for steps in getting access_token using oauth, you can take reference below 12 | // https://medium.com/@terence410/using-google-oauth-to-access-its-api-nodejs-b2678ade776f 13 | const drive3 = new TsGoogleDrive({oAuthCredentials: {access_token: ""}}); 14 | const drive4 = new TsGoogleDrive({oAuthCredentials: {refresh_token: ""}, oauthClientOptions: {clientId: "", clientSecret: ""}}); 15 | } 16 | 17 | async function getSingleFile() { 18 | const fileId = ""; 19 | const file = await tsGoogleDrive.getFile(fileId); 20 | if (file) { 21 | const isFolder = file.isFolder; 22 | } 23 | } 24 | 25 | async function listFolders() { 26 | const folderId = ""; 27 | const folders = await tsGoogleDrive 28 | .query() 29 | .setFolderOnly() 30 | .inFolder(folderId) 31 | .run(); 32 | } 33 | 34 | async function createFolder() { 35 | const folderId = ""; 36 | const newFolder = await tsGoogleDrive.createFolder({ 37 | name: "testing", 38 | parent: folderId, 39 | }); 40 | 41 | // try to search for it again 42 | const foundFolder = await tsGoogleDrive 43 | .query() 44 | .setFolderOnly() 45 | .setModifiedTime("=", newFolder.modifiedAt) 46 | .runOnce(); 47 | } 48 | 49 | async function uploadAndDownload() { 50 | const folderId = ""; 51 | const filename = "./icon.png"; 52 | const newFile = await tsGoogleDrive.upload(filename, {parent: folderId}); 53 | const downloadBuffer = await newFile.download(); 54 | 55 | // of if you want stream 56 | const drive = google.drive({version: "v3", auth: newFile.client}); 57 | const file = await drive.files.get({ 58 | fileId: newFile.id, 59 | alt: 'media' 60 | }, {responseType: "stream"}); 61 | 62 | file.data.on("data", data => { 63 | // stream data 64 | }); 65 | file.data.on("end", () => { 66 | // stream end 67 | }); 68 | 69 | // or use pipe 70 | const writeStream = fs.createWriteStream('./output.png'); 71 | file.data.pipe(writeStream); 72 | } 73 | 74 | async function search() { 75 | const folderId = ""; 76 | const query = await tsGoogleDrive 77 | .query() 78 | .setFolderOnly() 79 | .inFolder(folderId) 80 | .setPageSize(3) 81 | .setOrderBy("name") 82 | .setNameContains("New"); 83 | 84 | // or you can use any query https://developers.google.com/drive/api/v3/search-files 85 | query.setQuery("name = 'hello'"); 86 | 87 | while (query.hasNextPage()) { 88 | const folders = await query.run(); 89 | for (const folder of folders) { 90 | await folder.delete(); 91 | } 92 | } 93 | } 94 | 95 | async function emptyTrash() { 96 | const trashedFiles = await tsGoogleDrive 97 | .query() 98 | .inTrash() 99 | .run(); 100 | 101 | await tsGoogleDrive.emptyTrash(); 102 | } 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Terence Tsang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Drive API Library # 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Test][github-action-image]][github-action-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | 7 | [npm-image]: https://img.shields.io/npm/v/ts-google-drive.svg 8 | [npm-url]: https://npmjs.org/package/ts-google-drive 9 | [github-action-image]: https://github.com/terence410/ts-google-drive/workflows/Testing/badge.svg 10 | [github-action-url]: https://github.com/terence410/ts-google-drive/actions 11 | [codecov-image]: https://img.shields.io/codecov/c/github/terence410/ts-google-drive.svg?style=flat-square 12 | [codecov-url]: https://codecov.io/gh/terence410/ts-google-drive 13 | 14 | Manage Google Drive easily. Support create folders, upload files, download files, searching, etc.. 15 | The library is build with [Google Drive API v3](https://developers.google.com/drive/api/v3/about-sdk). 16 | 17 | # Features 18 | 19 | - Create Folders 20 | - Upload Files 21 | - Download Files (as Buffer) 22 | - Powerful file query tools 23 | - Empty Trash 24 | 25 | # Usage 26 | ```typescript 27 | import {TsGoogleDrive} from "ts-google-drive"; 28 | import {google} from "googleapis"; 29 | const tsGoogleDrive = new TsGoogleDrive({keyFilename: "serviceAccount.json"}); 30 | 31 | async function auth() { 32 | const drive1 = new TsGoogleDrive({keyFilename: "serviceAccount.json"}); 33 | const drive2 = new TsGoogleDrive({credentials: {client_email: "", private_key: ""}}); 34 | 35 | // https://www.npmjs.com/package/google-auth-library 36 | const drive3 = new TsGoogleDrive({oAuthCredentials: {access_token: ""}}); 37 | const drive4 = new TsGoogleDrive({oAuthCredentials: {refresh_token: ""}, oauthClientOptions: {clientId: "", clientSecret: ""}}); 38 | } 39 | 40 | async function getSingleFile() { 41 | const fileId = ""; 42 | const file = await tsGoogleDrive.getFile(fileId); 43 | if (file) { 44 | const isFolder = file.isFolder; 45 | } 46 | } 47 | 48 | async function listFolders() { 49 | const folderId = ""; 50 | const folders = await tsGoogleDrive 51 | .query() 52 | .setFolderOnly() 53 | .inFolder(folderId) 54 | .run(); 55 | } 56 | 57 | async function createFolder() { 58 | const folderId = ""; 59 | const newFolder = await tsGoogleDrive.createFolder({ 60 | name: "testing", 61 | parent: folderId, 62 | }); 63 | 64 | // try to search for it again 65 | const foundFolder = await tsGoogleDrive 66 | .query() 67 | .setFolderOnly() 68 | .setModifiedTime("=", newFolder.modifiedAt) 69 | .runOnce(); 70 | } 71 | 72 | async function uploadAndDownload() { 73 | const folderId = ""; 74 | const filename = "./icon.png"; 75 | const newFile = await tsGoogleDrive.upload(filename, {parent: folderId}); 76 | const downloadBuffer = await newFile.download(); 77 | 78 | // you have to use "googleapis" package 79 | // of if you want stream 80 | const drive = google.drive({version: "v3", auth: newFile.client}); 81 | const file = await drive.files.get({ 82 | fileId: newFile.id, 83 | alt: 'media' 84 | }, {responseType: "stream"}); 85 | 86 | file.data.on("data", data => { 87 | // stream data 88 | }); 89 | file.data.on("end", () => { 90 | // stream end 91 | }); 92 | 93 | // or use pipe 94 | const writeStream = fs.createWriteStream('./output.png'); 95 | file.data.pipe(writeStream); 96 | } 97 | 98 | async function search() { 99 | const folderId = ""; 100 | const query = await tsGoogleDrive 101 | .query() 102 | .setFolderOnly() 103 | .inFolder(folderId) 104 | .setPageSize(3) 105 | .setOrderBy("name") 106 | .setNameContains("New"); 107 | 108 | // or you can use any query https://developers.google.com/drive/api/v3/search-files 109 | query.setQuery("name = 'hello'"); 110 | 111 | while (query.hasNextPage()) { 112 | const folders = await query.run(); 113 | for (const folder of folders) { 114 | await folder.delete(); 115 | } 116 | } 117 | } 118 | 119 | async function emptyTrash() { 120 | await tsGoogleDrive.emptyTrash(); 121 | } 122 | ``` 123 | 124 | # Using Service Account 125 | 126 | - Create a Google Cloud Project 127 | - [Create Service Account](https://console.cloud.google.com/iam-admin/serviceaccounts/create) 128 | - Service account details > Choose any service account name > CREATE 129 | - Grant this service account access to project > CONTINUE 130 | - Grant users access to this service account ( > CREATE KEY 131 | - Save the key file into your project 132 | - Enable Drive API 133 | - [APIs and Services](https://console.cloud.google.com/apis/dashboard) > Enable APIS AND SERVICES 134 | - Search Google Drive API > Enable 135 | - To access shared folder 136 | - Open the JSON key file, you will find an email xxx@xxx.iam.gserviceaccount.com. 137 | - Go to your Google Drive Folder and shared the edit permission to the email address. 138 | - Create using serviceAccount.json 139 | ```typescript 140 | const drive1 = new TsGoogleDrive({keyFilename: "serviceAccount.json"}); 141 | ``` 142 | - Create using client_email and private_key (which is available inside the services account JSON) 143 | ```typescript 144 | const drive2 = new TsGoogleDrive({credentials: {client_email: "", private_key: ""}}); 145 | ``` 146 | 147 | # Using OAuth 148 | - You can take reference on below link how to grab an oauth access token 149 | - https://medium.com/@terence410/using-google-oauth-to-access-its-api-nodejs-b2678ade776f 150 | ```typescript 151 | const drive3 = new TsGoogleDrive({oAuthCredentials: {access_token: ""}}); 152 | const drive4 = new TsGoogleDrive({oAuthCredentials: {refresh_token: ""}, oauthClientOptions: {clientId: "", clientSecret: ""}}); 153 | ``` 154 | 155 | # Links 156 | - https://www.npmjs.com/package/googleapis 157 | - https://www.npmjs.com/package/google-auth-library 158 | - https://developers.google.com/drive 159 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terence410/ts-google-drive/faf7dd1890ca26131ec1faeab4069abf02b06310/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Terence", 3 | "name": "ts-google-drive", 4 | "description": "Manage google drive easily. Support create folders, upload files, download files, searching, etc..", 5 | "version": "0.0.7", 6 | "homepage": "https://github.com/terence410/ts-google-drive", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/terence410/ts-google-drive.git" 10 | }, 11 | "main": "build/index.js", 12 | "engines": { 13 | "node": ">=6.0" 14 | }, 15 | "scripts": { 16 | "test": "mocha --exit --timeout 30000 -r ts-node/register tests/*.ts", 17 | "test:coverage": "nyc --reporter=json --reporter=text mocha --exit --timeout 30000 -r ts-node/register tests/*.ts", 18 | "npm:patch": "npm version patch", 19 | "build": "tsc" 20 | }, 21 | "dependencies": { 22 | "google-auth-library": "^8.7.0" 23 | }, 24 | "devDependencies": { 25 | "@types/chai": "^4.3.4", 26 | "@types/mocha": "^10.0.1", 27 | "@types/node": "^18.11.10", 28 | "chai": "^4.3.7", 29 | "dotenv": "^16.0.3", 30 | "mocha": "^10.1.0", 31 | "ts-node": "^10.9.1", 32 | "typescript": "^4.9.3", 33 | "nyc": "^15.1.0", 34 | "googleapis": "^109.0.1" 35 | }, 36 | "keywords": [ 37 | "google drive", 38 | "upload file", 39 | "drive api v3", 40 | "google cloud" 41 | ], 42 | "license": "MIT License" 43 | } 44 | -------------------------------------------------------------------------------- /src/AuthClientBase.ts: -------------------------------------------------------------------------------- 1 | import {BaseExternalAccountClient, GoogleAuth, OAuth2Client} from "google-auth-library"; 2 | import {AuthClient} from "google-auth-library/build/src/auth/authclient"; 3 | import {ITsGoogleDriveOptions} from "./types"; 4 | 5 | const scopes = ["https://www.googleapis.com/auth/drive"]; 6 | 7 | export class AuthClientBase { 8 | private _client?: OAuth2Client | BaseExternalAccountClient; // hide from the object 9 | 10 | constructor(public readonly options: ITsGoogleDriveOptions) { 11 | // hide the property from printing 12 | Object.defineProperty(this, "_client", { 13 | enumerable: false, 14 | writable: true, 15 | value: undefined, 16 | }); 17 | } 18 | 19 | protected async _getClient(): Promise { 20 | if (!this._client) { 21 | if ("oAuthCredentials" in this.options) { 22 | const oauth = new OAuth2Client(this.options.oauthClientOptions); 23 | oauth.setCredentials(this.options.oAuthCredentials); 24 | this._client = oauth; 25 | 26 | } else { 27 | const googleAuth = new GoogleAuth({...this.options, scopes}); 28 | this._client = await googleAuth.getClient(); 29 | } 30 | } 31 | 32 | return this._client!; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/File.ts: -------------------------------------------------------------------------------- 1 | import {BaseExternalAccountClient, OAuth2Client} from "google-auth-library"; 2 | import {AuthClient} from "google-auth-library/build/src/auth/authclient"; 3 | import {FILE_FIELDS, GOOGLE_DRIVE_API, IUpdateMetaOptions} from "./types"; 4 | 5 | export class File { 6 | public id: string = ""; 7 | public name: string = ""; 8 | public mimeType: string = ""; 9 | public kind: string = ""; 10 | public modifiedTime: string = ""; 11 | public createdTime: string = ""; 12 | public size: number = 0; 13 | public parents: string[] = []; 14 | 15 | constructor(public client: OAuth2Client | BaseExternalAccountClient) { 16 | // hide the property from printing 17 | Object.defineProperty(this, "client", { 18 | enumerable: false, 19 | writable: false, 20 | value: client, 21 | }); 22 | } 23 | 24 | public get modifiedAt() { 25 | return new Date(this.modifiedTime); 26 | } 27 | 28 | public get createdAt() { 29 | return new Date(this.createdTime); 30 | } 31 | 32 | public get isFolder() { 33 | return this.mimeType === "application/vnd.google-apps.folder"; 34 | } 35 | 36 | // https://developers.google.com/drive/api/v3/manage-downloads 37 | public async download(): Promise { 38 | const client = this._getClient(); 39 | const url = `/files/${this.id}`; 40 | const params = {alt: "media"}; 41 | 42 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, params, responseType: "arraybuffer"}); 43 | return Buffer.from(res.data as any); 44 | } 45 | 46 | // https://developers.google.com/drive/api/v3/manage-downloads 47 | public async createStream(): Promise { 48 | const client = this._getClient(); 49 | const url = `/files/${this.id}`; 50 | const params = {alt: "media"}; 51 | 52 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, params, responseType: "arraybuffer"}); 53 | return Buffer.from(res.data as any); 54 | } 55 | 56 | // https://developers.google.com/drive/api/v3/reference/files/update 57 | public async update(options: IUpdateMetaOptions = {}) { 58 | const client = this._getClient(); 59 | const url = `/files/${this.id}`; 60 | const params = {fields: FILE_FIELDS, addParents: options.parent, description: options.description}; 61 | const body: any = options; 62 | 63 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, method: "PATCH", params, data: body}); 64 | Object.assign(this, res.data); 65 | } 66 | 67 | // https://developers.google.com/drive/api/v3/reference/files/delete 68 | public async delete() { 69 | const client = this._getClient(); 70 | const url = `/files/${this.id}`; 71 | const params = {}; 72 | 73 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, method: "DELETE", params}); 74 | return true; 75 | } 76 | 77 | private _getClient(): AuthClient { 78 | return this.client; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Query.ts: -------------------------------------------------------------------------------- 1 | import {AuthClientBase} from "./AuthClientBase"; 2 | import {File} from "./File"; 3 | import {FILE_FIELDS, GOOGLE_DRIVE_API, ITsGoogleDriveOptions} from "./types"; 4 | 5 | type IOperator = "=" | ">" | ">=" | "<" | "<="; 6 | type orderByKey = "createdTime" | 7 | "folder" | 8 | "modifiedByMeTime" | 9 | "modifiedTime" | 10 | "name" | 11 | "name_natural" | 12 | "quotaBytesUsed" | 13 | "recency" | 14 | "sharedWithMeTime" | 15 | "starred" | 16 | "viewedByMeTime"; 17 | 18 | // https://developers.google.com/drive/api/v3/reference/files/list 19 | // https://developers.google.com/drive/api/v3/search-files 20 | export class Query extends AuthClientBase { 21 | public queries: string[] = []; 22 | public pageSize: number = 100; 23 | public orderBy: string[] = []; 24 | 25 | private nextPageToken?: string; 26 | 27 | constructor(options: ITsGoogleDriveOptions) { 28 | super(options); 29 | } 30 | 31 | public hasNextPage() { 32 | return this.nextPageToken === undefined || !!this.nextPageToken; 33 | } 34 | 35 | public setPageSize(value: number) { 36 | this.pageSize = value; 37 | return this; 38 | } 39 | 40 | public setOrderBy(value: orderByKey | orderByKey[]) { 41 | if (Array.isArray(value)) { 42 | this.orderBy = this.orderBy.concat(value); 43 | } else { 44 | this.orderBy.push(value); 45 | } 46 | 47 | return this; 48 | } 49 | 50 | public setFolderOnly() { 51 | this.queries.push("mimeType='application/vnd.google-apps.folder'"); 52 | return this; 53 | } 54 | 55 | public setFileOnly() { 56 | this.queries.push("mimeType!='application/vnd.google-apps.folder'"); 57 | return this; 58 | } 59 | 60 | public setFullTextContains(name: string) { 61 | this.queries.push(`fullText contains '${name}'`); 62 | return this; 63 | } 64 | 65 | public setNameContains(name: string) { 66 | this.queries.push(`name contains '${name}'`); 67 | return this; 68 | } 69 | 70 | public setNameEqual(name: string) { 71 | this.queries.push(`name = '${name}'`); 72 | return this; 73 | } 74 | 75 | public setModifiedTime(operator: IOperator, date: Date) { 76 | this.queries.push(`modifiedTime ${operator} '${date.toISOString()}'`); 77 | return this; 78 | } 79 | 80 | public setCreatedTime(operator: IOperator, date: Date) { 81 | this.queries.push(`createdTime ${operator} '${date.toISOString()}'`); 82 | return this; 83 | } 84 | 85 | public setQuery(query: string) { 86 | this.queries.push(query); 87 | return this; 88 | } 89 | 90 | public inFolder(folderId: string) { 91 | this.queries.push(`'${folderId}' in parents`); 92 | return this; 93 | } 94 | 95 | public inTrash() { 96 | this.queries.push(`trashed = true`); 97 | return this; 98 | } 99 | 100 | public async runOnce() { 101 | this.setPageSize(1); 102 | const list = await this.run(); 103 | return list.length ? list[0] : undefined; 104 | } 105 | 106 | public async run() { 107 | // if the next page token is "" 108 | if (this.nextPageToken === "") { 109 | throw new Error("The query has no more next page."); 110 | } 111 | 112 | const client = await this._getClient(); 113 | const url = `/files`; 114 | const params = { 115 | q: this.queries.join(" and "), 116 | spaces: "drive", 117 | pageSize: this.pageSize, 118 | pageToken: this.nextPageToken, 119 | fields: `kind,nextPageToken,incompleteSearch,files(${FILE_FIELDS})`, 120 | orderBy: this.orderBy.join(","), 121 | }; 122 | 123 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, params}); 124 | const result = res.data as any; 125 | 126 | // update next page token, we must at least mark it into empty 127 | this.nextPageToken = result.nextPageToken || ""; 128 | 129 | // convert to files 130 | const list: File[] = []; 131 | if (result.files && Array.isArray(result.files)) { 132 | for (const item of result.files) { 133 | const file = new File(client); 134 | Object.assign(file, item); 135 | list.push(file); 136 | } 137 | } 138 | 139 | return list; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/TsGoogleDrive.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import {AuthClientBase} from "./AuthClientBase"; 4 | import {File} from "./File"; 5 | import {Query} from "./Query"; 6 | import { 7 | FILE_FIELDS, 8 | GOOGLE_DRIVE_API, 9 | GOOGLE_DRIVE_UPLOAD_API, 10 | ICreateFolderOptions, 11 | ITsGoogleDriveOptions, 12 | IUpdateMetaOptions 13 | } from "./types"; 14 | 15 | export class TsGoogleDrive extends AuthClientBase { 16 | constructor(options: ITsGoogleDriveOptions) { 17 | super(options); 18 | } 19 | 20 | public query() { 21 | return new Query(this.options); 22 | } 23 | 24 | // https://developers.google.com/drive/api/v3/reference/files/get 25 | public async getFile(id: string) { 26 | const client = await this._getClient(); 27 | const url = `/files/${id}`; 28 | const params = {fields: FILE_FIELDS}; 29 | 30 | try { 31 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, params}); 32 | const file = new File(client); 33 | Object.assign(file, res.data); 34 | return file; 35 | } catch (err) { 36 | if (typeof err === "object" && (err as any)?.code === 404) { 37 | return undefined; 38 | } 39 | 40 | throw err; 41 | } 42 | } 43 | 44 | // https://developers.google.com/drive/api/v3/reference/files/create 45 | public async createFolder(options: ICreateFolderOptions = {}) { 46 | const client = await this._getClient(); 47 | const url = `/files`; 48 | const params = {fields: FILE_FIELDS}; 49 | const data: any = {mimeType: "application/vnd.google-apps.folder", name: options.name, description: options.description}; 50 | if (options.parent) { 51 | data.parents = [options.parent]; 52 | } 53 | 54 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, params, data, method: "POST"}); 55 | const file = new File(client); 56 | Object.assign(file, res.data); 57 | return file; 58 | } 59 | 60 | // https://developers.google.com/drive/api/v3/reference/files/create 61 | public async upload(filename: string, options: IUpdateMetaOptions = {}) { 62 | const client = await this._getClient(); 63 | const params = {uploadType: "media", fields: FILE_FIELDS}; 64 | 65 | // upload 66 | const buffer = fs.readFileSync(filename); 67 | const res = await client.request({url: GOOGLE_DRIVE_UPLOAD_API, method: "POST", params, body: buffer}); 68 | 69 | // create file 70 | const file = new File(client); 71 | Object.assign(file, res.data); 72 | 73 | // update meta 74 | if (!options.name) { 75 | options.name = path.basename(filename); 76 | } 77 | await file.update(options); 78 | 79 | return file; 80 | } 81 | 82 | // https://developers.google.com/drive/api/v3/reference/files/emptyTrash 83 | public async emptyTrash() { 84 | const client = await this._getClient(); 85 | const url = `/files/trash`; 86 | const params = {}; 87 | const res = await client.request({baseURL: GOOGLE_DRIVE_API, url, method: "DELETE", params}); 88 | return true; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {TsGoogleDrive} from "./TsGoogleDrive"; 2 | export {TsGoogleDrive}; 3 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {Credentials, GoogleAuthOptions, OAuth2ClientOptions} from "google-auth-library"; 2 | 3 | export const GOOGLE_DRIVE_API = "https://www.googleapis.com/drive/v3"; 4 | export const GOOGLE_DRIVE_UPLOAD_API = "https://www.googleapis.com/upload/drive/v3/files"; 5 | export const FILE_FIELDS = "id,kind,name,mimeType,parents,modifiedTime,createdTime,size"; 6 | 7 | export type ITsGoogleDriveOptions = GoogleAuthOptions | {oAuthCredentials: Credentials, oauthClientOptions?: OAuth2ClientOptions}; 8 | 9 | export type IUpdateMetaOptions = { 10 | name?: string; 11 | parent?: string; 12 | description?: string; 13 | }; 14 | 15 | export type ICreateFolderOptions = { 16 | name?: string; 17 | parent?: string; 18 | description?: string; 19 | }; 20 | 21 | type ISearchFileOptions = { 22 | folderOnly?: boolean; 23 | fileOnly?: boolean; 24 | nameContains?: string; 25 | query?: string; 26 | inParents?: string | number; 27 | }; 28 | -------------------------------------------------------------------------------- /tests/general.test.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from "buffer"; 2 | import {config} from "dotenv"; 3 | config(); 4 | 5 | import { assert, expect } from "chai"; 6 | import * as fs from "fs"; 7 | import "mocha"; 8 | import {TsGoogleDrive} from "../src/TsGoogleDrive"; 9 | import {google} from "googleapis"; 10 | 11 | const folderId = process.env.FOLDER_ID || ""; 12 | const keyFilename = process.env.KEY_FILENAME || ""; 13 | const clientEmail = process.env.CLIENT_EMAIL; 14 | const privateKey = process.env.PRIVATE_KEY; 15 | const accessToken = process.env.ACCESS_TOKEN; 16 | const credentials = clientEmail && privateKey ? {client_email: clientEmail, private_key: privateKey} : undefined; 17 | const tsGoogleDrive = new TsGoogleDrive({keyFilename, credentials}); 18 | let testFolderId = ""; 19 | 20 | const timeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 21 | 22 | describe("Testing", () => { 23 | // create test folder 24 | before(async () => { 25 | const newFolder = await tsGoogleDrive.createFolder({ 26 | name: "testing", 27 | parent: folderId, 28 | }); 29 | assert.isTrue(newFolder.isFolder); 30 | assert.isTrue(newFolder.parents.includes(folderId)); 31 | 32 | // assign into testFolderId for further testing 33 | testFolderId = newFolder.id; 34 | 35 | // wait for a while for the folder to be able to be searched 36 | await timeout(3000); 37 | }); 38 | 39 | // remove test folder 40 | after(async () => { 41 | const testFolder = await tsGoogleDrive.getFile(testFolderId); 42 | if (testFolder) { 43 | await testFolder.delete(); 44 | } 45 | }); 46 | 47 | it("get folder", async () => { 48 | const testFolder = await tsGoogleDrive.getFile(testFolderId); 49 | assert.isDefined(testFolder); 50 | if (testFolder) { 51 | assert.isTrue(testFolder.isFolder); 52 | } 53 | 54 | const newFile = await tsGoogleDrive.getFile("unknown"); 55 | assert.isUndefined(newFile); 56 | }); 57 | 58 | it("create folder", async () => { 59 | const newFolder = await tsGoogleDrive.createFolder({ 60 | name: "testing", 61 | parent: testFolderId, 62 | }); 63 | assert.isTrue(newFolder.isFolder); 64 | assert.isTrue(newFolder.parents.includes(testFolderId)); 65 | 66 | // wait for a while for the folder to appear 67 | await timeout(3000); 68 | 69 | // try to search for it 70 | const foundFolder1 = await tsGoogleDrive 71 | .query() 72 | .setFolderOnly() 73 | .setModifiedTime("=", newFolder.modifiedAt) 74 | .runOnce(); 75 | assert.isDefined(foundFolder1); 76 | 77 | // try to search for it 78 | const foundFolder2 = await tsGoogleDrive 79 | .query() 80 | .setFolderOnly() 81 | .setModifiedTime(">", newFolder.modifiedAt) 82 | .runOnce(); 83 | assert.isUndefined(foundFolder2); 84 | }); 85 | 86 | it("upload file", async () => { 87 | const filename = "./icon.png"; 88 | const buffer = fs.readFileSync(filename); 89 | const newFile = await tsGoogleDrive.upload(filename, {parent: testFolderId}); 90 | assert.isDefined(newFile); 91 | assert.isTrue(newFile.parents.includes(testFolderId)); 92 | 93 | const downloadBuffer = await newFile.download(); 94 | assert.deepEqual(buffer, downloadBuffer); 95 | }); 96 | 97 | it("download with stream", async () => { 98 | const filename = "./icon.png"; 99 | const buffer = fs.readFileSync(filename); 100 | const newFile = await tsGoogleDrive.upload(filename, {parent: testFolderId}); 101 | assert.isDefined(newFile); 102 | assert.isTrue(newFile.parents.includes(testFolderId)); 103 | 104 | // download by stream 105 | const drive = google.drive({version: "v3", auth: newFile.client}); 106 | const file = await drive.files.get({ 107 | fileId: newFile.id, 108 | alt: 'media' 109 | }, {responseType: "stream"}); 110 | 111 | let downloadBuffer = Buffer.alloc(0); 112 | file.data.on("data", data => { 113 | downloadBuffer = Buffer.concat([downloadBuffer, data]); 114 | }); 115 | // wait for it to be completed 116 | await new Promise(resolve => file.data.on("end", resolve)); 117 | 118 | // check if they are the same 119 | assert.deepEqual(buffer, downloadBuffer); 120 | }); 121 | 122 | it("search folders", async () => { 123 | const folders = await tsGoogleDrive 124 | .query() 125 | .setFolderOnly() 126 | .inFolder(testFolderId) 127 | .run(); 128 | assert.isArray(folders); 129 | 130 | for (const item of folders) { 131 | assert.isTrue(item.isFolder); 132 | assert.isTrue(item.parents.includes(testFolderId)); 133 | await item.delete(); 134 | } 135 | }); 136 | 137 | it("search files", async () => { 138 | const filename = "icon.png"; 139 | const files = await tsGoogleDrive 140 | .query() 141 | .setFileOnly() 142 | .inFolder(testFolderId) 143 | .setNameEqual(filename) 144 | .run(); 145 | 146 | for (const item of files) { 147 | assert.isFalse(item.isFolder); 148 | assert.equal(item.name, filename); 149 | assert.isTrue(item.parents.includes(testFolderId)); 150 | await item.delete(); 151 | } 152 | }); 153 | 154 | 155 | it("search with paging", async () => { 156 | const total = 5; 157 | const promises = Array(total).fill(0).map((x, i) => { 158 | return tsGoogleDrive.createFolder({parent: testFolderId, name: "New" + i}); 159 | }); 160 | await Promise.all(promises); 161 | 162 | // wait a while first 163 | await timeout(3000); 164 | 165 | const query = await tsGoogleDrive 166 | .query() 167 | .setFolderOnly() 168 | .inFolder(testFolderId) 169 | .setPageSize(4) 170 | .setOrderBy("name") 171 | .setNameContains("New"); 172 | 173 | while (query.hasNextPage()) { 174 | const folders = await query.run(); 175 | const deletePromises = folders.map(x => { 176 | assert.isTrue(x.parents.includes(testFolderId)); 177 | return x.delete(); 178 | }); 179 | await Promise.all(deletePromises); 180 | } 181 | }); 182 | 183 | it("empty trash", async () => { 184 | const trashedFiles = await tsGoogleDrive 185 | .query() 186 | .inTrash() 187 | .run(); 188 | 189 | await tsGoogleDrive.emptyTrash(); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | "typeRoots": ["./src/@types", "./node_modules/@types"], 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "include": [ 64 | "src/**/*" 65 | ], 66 | "exclude": [ 67 | "node_modules", 68 | "**/*.spec.ts" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "semicolon": [true, "always"], 9 | "no-trailing-whitespace": false, 10 | "arrow-parens": false, 11 | "no-console": false, 12 | "max-classes-per-file": 1, 13 | "interface-over-type-literal": false, 14 | "max-line-length": [100], 15 | "no-bitwise": false, 16 | "object-literal-sort-keys": false, 17 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"] 18 | }, 19 | "rulesDirectory": [] 20 | } 21 | --------------------------------------------------------------------------------