├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------