├── web
├── .browserslistrc
├── src
│ ├── style.css
│ ├── assets
│ │ ├── logo.png
│ │ ├── README.md
│ │ └── logo.svg
│ ├── api.js
│ ├── plugins
│ │ └── vuetify.js
│ ├── i18n.js
│ ├── locales
│ │ ├── zh-CN.js
│ │ ├── zh-TW.js
│ │ └── en.js
│ ├── components
│ │ ├── PdfViewer.vue
│ │ ├── EpubViewer.vue
│ │ ├── LoginDialog.vue
│ │ ├── VideoViewer.vue
│ │ ├── FileUploadDialog.vue
│ │ └── FileViewer.vue
│ ├── main.js
│ ├── router.js
│ ├── App.vue
│ └── xfetch.js
├── babel.config.js
├── postcss.config.js
├── .prettierrc
├── dist
│ ├── fonts
│ │ ├── materialdesignicons-webfont.eot
│ │ ├── materialdesignicons-webfont.ttf
│ │ ├── materialdesignicons-webfont.woff
│ │ └── materialdesignicons-webfont.woff2
│ └── index.html
├── vue.config.js
├── .eslintrc.js
├── README.md
└── package.json
├── worker
├── .babelrc
├── package.json
├── bili.config.js
├── router.js
├── xfetch.js
├── googleDrive.js
└── index.js
├── .editorconfig
├── .prettierrc
├── .github
└── ISSUE_TEMPLATE
│ ├── custom.md
│ └── bug-report-------.md
├── code-builder
├── package.json
├── Dockerfile
├── index.js
├── index.html
└── yarn.lock
├── LICENSE
├── README.zh.md
├── README.zhtw.md
├── .gitignore
└── README.md
/web/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/web/src/style.css:
--------------------------------------------------------------------------------
1 | .pointer {
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/worker/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [],
3 | "plugins": []
4 | }
5 |
--------------------------------------------------------------------------------
/web/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/app'],
3 | }
4 |
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/web/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyao2q/GDIndex/master/web/src/assets/logo.png
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | tab_width = 4
6 | indent_style = tab
7 | insert_final_newline = true
8 |
--------------------------------------------------------------------------------
/web/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "tabWidth": 4,
4 | "endOfLine": "lf",
5 | "singleQuote": true,
6 | "semi": false
7 | }
8 |
--------------------------------------------------------------------------------
/web/dist/fonts/materialdesignicons-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyao2q/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.eot
--------------------------------------------------------------------------------
/web/dist/fonts/materialdesignicons-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyao2q/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.ttf
--------------------------------------------------------------------------------
/web/dist/fonts/materialdesignicons-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyao2q/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.woff
--------------------------------------------------------------------------------
/web/dist/fonts/materialdesignicons-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyao2q/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.woff2
--------------------------------------------------------------------------------
/web/src/assets/README.md:
--------------------------------------------------------------------------------
1 | `epub-reader.html` is the inlined version of https://github.com/maple3142/epubjs-reader, and the tool I used https://github.com/remy/inliner
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "trailingComma": "none",
5 | "singleQuote": true,
6 | "useTabs": true,
7 | "tabWidth": 4,
8 | "arrowParens": "avoid"
9 | }
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/web/src/api.js:
--------------------------------------------------------------------------------
1 | import xf from './xfetch'
2 |
3 | const headers = {}
4 | if (localStorage.token) {
5 | headers.Authorization = 'Basic ' + localStorage.token
6 | }
7 | export default xf.extend({
8 | baseURI: window.props.api,
9 | headers,
10 | })
11 |
--------------------------------------------------------------------------------
/code-builder/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gdindex-code-builder",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "body-parser": "^1.19.0",
8 | "express": "^4.17.1",
9 | "xfetch-js": "^0.5.0"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/code-builder/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12-alpine AS build
2 | WORKDIR /app
3 | COPY package.json .
4 | RUN yarn
5 | COPY . .
6 |
7 | FROM gcr.io/distroless/nodejs:12
8 | WORKDIR /app
9 | COPY --from=build /app .
10 | ENV PORT=8080
11 | EXPOSE 8080
12 | CMD ["index.js"]
13 |
--------------------------------------------------------------------------------
/web/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pluginOptions: {
3 | i18n: {
4 | locale: 'en',
5 | fallbackLocale: 'en',
6 | localeDir: 'locales',
7 | enableInSFC: false,
8 | },
9 | },
10 | filenameHashing: false,
11 | configureWebpack: {
12 | optimization: {
13 | splitChunks: false,
14 | },
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/web/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import '@mdi/font/css/materialdesignicons.min.css'
2 | import Vue from 'vue'
3 | import Vuetify from 'vuetify/lib'
4 | import i18n from '../i18n'
5 |
6 | Vue.use(Vuetify)
7 |
8 | export default new Vuetify({
9 | icons: {
10 | iconfont: 'mdi',
11 | },
12 | lang: {
13 | t: (key, ...params) => i18n.t(key, params),
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: ['plugin:vue/essential', '@vue/prettier'],
7 | rules: {
8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
10 | },
11 | parserOptions: {
12 | parser: 'babel-eslint',
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/web/src/i18n.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 | import en from './locales/en'
4 | import zhTW from './locales/zh-TW'
5 | import zhCN from './locales/zh-CN'
6 |
7 | Vue.use(VueI18n)
8 |
9 | export default new VueI18n({
10 | locale: navigator.language,
11 | fallbackLocale: 'en',
12 | messages: {
13 | en,
14 | 'zh-TW': zhTW,
15 | 'zh-HK': zhTW,
16 | 'zh-CN': zhCN,
17 | zh: zhCN,
18 | },
19 | })
20 |
--------------------------------------------------------------------------------
/web/src/locales/zh-CN.js:
--------------------------------------------------------------------------------
1 | import $vuetify from 'vuetify/es5/locale/zh-Hans'
2 |
3 | export default {
4 | fileName: '文件名称',
5 | modifiedTime: '修改时间',
6 | fileSize: '文件大小',
7 | mainDrive: '主硬盘',
8 | search: '搜索',
9 | fileUpload: '上传文件',
10 | urlUpload: '从网址上传',
11 | upload: '上传',
12 | fileToUpload: '要上传的文件',
13 | uploading: '正在上传...',
14 | serverProcessing: '服务器正在处理文件',
15 | bigFileUploadWarning:
16 | '由于 CloudFlare Workers 的限制,上传大档案可能会随机失败',
17 | $vuetify,
18 | }
19 |
--------------------------------------------------------------------------------
/web/src/locales/zh-TW.js:
--------------------------------------------------------------------------------
1 | import $vuetify from 'vuetify/es5/locale/zh-Hant'
2 |
3 | export default {
4 | fileName: '檔案名稱',
5 | modifiedTime: '修改時間',
6 | fileSize: '檔案大小',
7 | mainDrive: '主要硬碟',
8 | search: '搜尋',
9 | fileUpload: '檔案上傳',
10 | urlUpload: '從網址上傳',
11 | upload: '上傳',
12 | fileToUpload: '要上傳的檔案',
13 | uploading: '上傳中...',
14 | serverProcessing: '伺服器正在處理檔案',
15 | bigFileUploadWarning:
16 | '由於 CloudFlare Workers 的限制,上傳大檔案可能會隨機失敗',
17 | $vuetify,
18 | }
19 |
--------------------------------------------------------------------------------
/worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gdindex-worker",
3 | "version": "0.1.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "@sagi.io/workers-jwt": "^0.0.10",
8 | "buffer": "^6.0.2",
9 | "mime": "^2.4.6",
10 | "path-to-regexp": "^6.2.0"
11 | },
12 | "devDependencies": {
13 | "bili": "<5",
14 | "rollup-plugin-node-builtins": "^2.1.2",
15 | "rollup-plugin-node-globals": "^1.4.0"
16 | },
17 | "scripts": {
18 | "build": "bili"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # gdindex
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn run build
16 | ```
17 |
18 | ### Run your tests
19 | ```
20 | yarn run test
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | yarn run lint
26 | ```
27 |
28 | ### Customize configuration
29 | See [Configuration Reference](https://cli.vuejs.org/config/).
30 |
--------------------------------------------------------------------------------
/web/src/locales/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | fileName: 'File Name',
3 | modifiedTime: 'Modified Time',
4 | fileSize: 'File Size',
5 | mainDrive: 'Main Drive',
6 | search: 'Search',
7 | fileUpload: 'File Upload',
8 | urlUpload: 'Upload from url',
9 | upload: 'Upload',
10 | fileToUpload: 'File to upload',
11 | uploading: 'Uploading...',
12 | serverProcessing: 'Server is processing the file now',
13 | bigFileUploadWarning:
14 | "Due to CloudFlare Workers' limitation, uploading bigfiles may randomly failed.",
15 | }
16 |
--------------------------------------------------------------------------------
/web/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 | Artboard 46
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report-------.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report / 錯誤回報
3 | about: You must follow this template or I will close this directly. / 請務必填寫此模板,否則我會直接關閉。
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 |
13 | # Bug Description / 問題描述
14 |
15 |
16 |
17 |
18 | # Environment / 環境
19 |
20 | * Browser version / 瀏覽器版本:
21 | * OS version / 作業系統版本:
22 |
--------------------------------------------------------------------------------
/web/src/components/PdfViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
21 |
27 |
--------------------------------------------------------------------------------
/web/src/main.js:
--------------------------------------------------------------------------------
1 | import './style.css'
2 | import Vue from 'vue'
3 | import App from './App.vue'
4 | import router from './router'
5 | import vuetify from './plugins/vuetify'
6 | import i18n from './i18n'
7 | import PortalVue from 'portal-vue'
8 |
9 | if (window.props.defaultRootId) {
10 | // backward compability
11 | window.props.default_root_id = window.props.defaultRootId
12 | }
13 |
14 | Vue.use(PortalVue)
15 |
16 | Vue.config.productionTip = false
17 |
18 | window.app = new Vue({
19 | router,
20 | vuetify,
21 | i18n,
22 | render: (h) => h(App, { props: window.props }),
23 | }).$mount('#app')
24 |
--------------------------------------------------------------------------------
/web/dist/index.html:
--------------------------------------------------------------------------------
1 |
GDIndex
--------------------------------------------------------------------------------
/web/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import FileViewer from './components/FileViewer.vue'
4 | import EpubViewer from './components/EpubViewer.vue'
5 | import VideoViewer from './components/VideoViewer.vue'
6 | import PdfViewer from './components/PdfViewer.vue'
7 |
8 | Vue.use(VueRouter)
9 | const router = new VueRouter({
10 | routes: [
11 | { path: '/~viewer/epub', component: EpubViewer },
12 | { path: '/~viewer/video', component: VideoViewer },
13 | { path: '/~viewer/pdf', component: PdfViewer },
14 | { path: '/:path(.*)', component: FileViewer },
15 | ],
16 | mode: 'history',
17 | })
18 |
19 | export default router
20 |
--------------------------------------------------------------------------------
/worker/bili.config.js:
--------------------------------------------------------------------------------
1 | import builtins from 'rollup-plugin-node-builtins'
2 | import globals from 'rollup-plugin-node-globals'
3 |
4 | module.exports = {
5 | input: 'index.js',
6 | output: {
7 | dir: 'dist',
8 | fileName: 'worker.js',
9 | format: 'iife'
10 | },
11 | minify: false,
12 | plugins: {
13 | 'node-globals': globals(),
14 | 'node-builtins': builtins()
15 | },
16 | target: 'browser',
17 | banner: `
18 | self.props = {
19 | title: 'GDIndex',
20 | default_root_id: 'root',
21 | client_id: '202264815644.apps.googleusercontent.com',
22 | client_secret: 'X4Z3ca8xfWDb1Voo-F9a7ZxJ',
23 | refresh_token: '',
24 | service_account: false,
25 | service_account_json: {},
26 | auth: false,
27 | user: '',
28 | pass: '',
29 | upload: false,
30 | lite: false
31 | };`.slice(1)
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 maple3142
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 |
--------------------------------------------------------------------------------
/web/src/components/EpubViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
38 |
44 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gdindex-web",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "serve": "vue-cli-service serve",
6 | "build": "vue-cli-service build",
7 | "lint": "vue-cli-service lint"
8 | },
9 | "dependencies": {
10 | "core-js": "^3.7.0",
11 | "date-fns": "^2.16.1",
12 | "inline-resource": "^0.1.7",
13 | "inliner": "^1.13.1",
14 | "portal-vue": "^2.1.6",
15 | "pretty-bytes": "^5.4.1",
16 | "viewerjs": "^1.8.0",
17 | "vue": "^2.6.12",
18 | "vue-i18n": "^8.22.1",
19 | "vue-router": "^3.4.9",
20 | "vuetify": "^2.3.17",
21 | "xfetch-js": "^0.5.0"
22 | },
23 | "devDependencies": {
24 | "@mdi/font": "^5.8.55",
25 | "@vue/cli-plugin-babel": "^4.5.8",
26 | "@vue/cli-plugin-eslint": "^4.5.8",
27 | "@vue/cli-service": "^4.5.8",
28 | "@vue/eslint-config-prettier": "^6.0.0",
29 | "babel-eslint": "^10.1.0",
30 | "eslint": "^7.13.0",
31 | "eslint-plugin-prettier": "^3.1.4",
32 | "eslint-plugin-vue": "^7.1.0",
33 | "material-design-icons-iconfont": "^6.1.0",
34 | "prettier": "^2.1.2",
35 | "raw-loader": "^4.0.2",
36 | "sass": "^1.29.0",
37 | "sass-loader": "^10.1.0",
38 | "vue-cli-plugin-i18n": "^1.0.1",
39 | "vue-cli-plugin-vuetify": "^2.0.7",
40 | "vue-template-compiler": "^2.6.12",
41 | "vuetify-loader": "^1.6.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # GDIndex
2 |
3 | 
4 |
5 | > GDIndex 是一个类似 [GOIndex](https://github.com/donwa/goindex) 的东西,可以在 CloudFlare Workers 上架设 Google Drive 的目录,并提供许多功能
6 | >
7 | > 另外,这个并不是从 GOIndex 修改来了,而是直接重写
8 |
9 | [Demo](https://gdindex-demo.maple3142.workers.dev/)
10 |
11 | ## 和 GOIndex 不同之处
12 |
13 | - 前端使用 Vue 完成
14 | - 查看图片不用另开新窗口
15 | - 视频播放器支持字幕(目前只支持 srt)
16 | - 支持在线阅读 PDF, EPUB
17 | - 不支持目录加密(.password)
18 | - 支持 Http Basic Auth
19 | - 无需修改程序,即可接入多个云端硬盘(个人、团队)
20 |
21 | ## 使用教学
22 |
23 | ### 简单、自动的方法
24 |
25 | 前往 [https://gdindex-code-builder.maple3142.net/](https://gdindex-code-builder.maple3142.net/)(英文) 并遵照它的指示。
26 |
27 | ### 手动的方法
28 |
29 | 1. 安装 [rclone](https://rclone.org/)
30 | 2. 设定 Google Drive: https://rclone.org/drive/
31 | 3. 执行 `rclone config file` 以找到你的 `rclone.conf`
32 | 4. 在 `rclone.conf` 中寻找 `refresh_token` 以及 `root_folder_id` (可选)
33 | 5. 复制 [worker/dist/worker.js](worker/dist/worker.js) 的内容到 CloudFlare Workers
34 | 6. 在脚本顶端填上 `refresh_token`, `root_folder_id` 以及其他的选项
35 | 7. 部署!
36 |
37 | ### 使用服务帐户
38 |
39 | 1. 创建一个服务帐户,一个相应的服务帐户密钥,然后从[Google Cloud Platform控制台]获取JSON(https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
40 | 2. 在props对象中,将`service_account_json`值替换为服务帐户JSON文件的内容,并将`service_account`设置为`true`。
41 | 3. 确保所涉及的服务帐户有权访问“ root_folder_id”中指定的文件夹
42 | 4. 部署
43 |
--------------------------------------------------------------------------------
/README.zhtw.md:
--------------------------------------------------------------------------------
1 | # GDIndex
2 |
3 | 
4 |
5 | > GDIndex 是一個類似 [GOIndex](https://github.com/donwa/goindex) 的東西,可以在 CloudFlare Workers 上架設 Google Drive 的目錄,並提供許多功能
6 | >
7 | > 另外,這個並不是從 GOIndex 修改來了,而是直接重寫
8 |
9 | [Demo](https://gdindex-demo.maple3142.workers.dev/)
10 |
11 | ## 和 GOIndex 不同之處
12 |
13 | - 前端使用 Vue 完成
14 | - 圖片檢視不用另開新頁面
15 | - 影片播放器支援字幕(目前只有 srt)
16 | - 線上 PDF, EPUB 閱讀器
17 | - 不支援目錄加密(.password)
18 | - 支援 Http Basic Auth
19 | - 支援多雲端硬碟(個人、團隊),不需要額外改程式設定
20 |
21 | ## 使用教學
22 |
23 | ### 簡單、自動的方法
24 |
25 | 前往 [https://gdindex-code-builder.maple3142.net/](https://gdindex-code-builder.maple3142.net/)(英文) 並遵照它的指示。
26 |
27 | ### 手動的方法
28 |
29 | 1. 安裝 [rclone](https://rclone.org/)
30 | 2. 設定 Google Drive: https://rclone.org/drive/
31 | 3. 執行 `rclone config file` 以找到你的 `rclone.conf`
32 | 4. 在 `rclone.conf` 中尋找 `refresh_token` 以及 `root_folder_id` (選擇性)
33 | 5. 複製 [worker/dist/worker.js](worker/dist/worker.js) 的內容到 CloudFlare Workers
34 | 6. 在腳本頂端填上 `refresh_token`, `root_folder_id` 以及其他的選項
35 | 7. 部署!
36 |
37 |
38 | ### 使用服務帳戶
39 |
40 | 1. 創建一個服務帳戶,一個對應的服務帳戶密鑰,然後從[Google Cloud Platform控制台]獲取JSON(https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
41 | 2. 在props對像中,將`service_account_json`值替換為服務帳戶JSON文件的內容,並將`service_account`設置為`true`。
42 | 3. 確保所涉及的服務帳戶有權訪問“ root_folder_id”中指定的文件夾
43 | 4. 部署
--------------------------------------------------------------------------------
/code-builder/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | const xf = require('xfetch-js')
4 |
5 | const app = express()
6 | app.use(
7 | bodyParser.urlencoded({
8 | extended: true
9 | })
10 | )
11 |
12 | app.get('/', (req, res) => {
13 | res.sendFile(__dirname + '/index.html')
14 | })
15 | function replace(t, a, b) {
16 | const reg = new RegExp(String.raw`(${a}: \').*?(\')`)
17 | return t.replace(reg, '$1' + b + '$2')
18 | }
19 | app.post('/getcode', async (req, res) => {
20 | const p = req.body
21 | const r = await xf
22 | .post('https://www.googleapis.com/oauth2/v4/token', {
23 | urlencoded: {
24 | code: p.auth_code,
25 | client_id: '202264815644.apps.googleusercontent.com',
26 | client_secret: 'X4Z3ca8xfWDb1Voo-F9a7ZxJ',
27 | redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
28 | grant_type: 'authorization_code'
29 | }
30 | })
31 | .json()
32 | .catch(e => null)
33 | if (r === null) {
34 | return res
35 | .status(400)
36 | .send(
37 | "Authorization Code is invalid. Perhaps it doesn's exists or it has been used for 1 time."
38 | )
39 | }
40 | let code = await xf
41 | .get(
42 | 'https://raw.githubusercontent.com/maple3142/GDIndex/master/worker/dist/worker.js'
43 | )
44 | .text()
45 | code = replace(code, 'refresh_token', r.refresh_token)
46 | for (const [k, v] of Object.entries(p)) {
47 | code = replace(code, k, v)
48 | }
49 | if (p.auth) {
50 | code = code.replace('auth: false', 'auth: true')
51 | }
52 | if (p.upload) {
53 | code = code.replace('upload: false', 'upload: true')
54 | }
55 | res.set('Content-Type', 'text/javascript; charset=utf-8')
56 | res.send(code)
57 | })
58 | app.listen(process.env.PORT)
59 |
--------------------------------------------------------------------------------
/web/src/components/LoginDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Require Authentiaction
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Login
34 |
35 |
36 |
37 |
38 |
39 |
83 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # next.js build output
79 | .next
80 |
81 | # nuxt.js build output
82 | .nuxt
83 |
84 | # gatsby files
85 | .cache/
86 | public
87 |
88 | # vuepress build output
89 | .vuepress/dist
90 |
91 | # Serverless directories
92 | .serverless/
93 |
94 | # FuseBox cache
95 | .fusebox/
96 |
97 | # DynamoDB Local files
98 | .dynamodb/
99 |
100 | # TernJS port file
101 | .tern-port
102 |
103 | worker/script.js
104 | worker/wrangler.toml
105 | **/node_modules/
106 |
--------------------------------------------------------------------------------
/web/src/components/VideoViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
74 |
81 |
--------------------------------------------------------------------------------
/worker/router.js:
--------------------------------------------------------------------------------
1 | const pathToRegexp = require('path-to-regexp')
2 |
3 | class Router {
4 | constructor() {
5 | this.handlers = []
6 | }
7 | use(handler) {
8 | this.handlers.push(handler)
9 | return this
10 | }
11 | useRoute(path, handler) {
12 | const keys = []
13 | const re = pathToRegexp(path, keys)
14 | this.use(async (req, res, next) => {
15 | if (re.test(req.pathname)) {
16 | const [_, ...result] = re.exec(req.pathname)
17 | const params = {}
18 | for (let i = 0; i < result.length; i++) {
19 | params[keys[i].name] = result[i]
20 | }
21 | req.params = params
22 | await handler(req, res, next)
23 | }
24 | })
25 | return this
26 | }
27 | useRouteWithVerb(verb, path, handler) {
28 | verb = verb.toUpperCase()
29 | this.useRoute(path, async (req, res, next) => {
30 | if (req.method === verb) {
31 | await handler(req, res, next)
32 | }
33 | })
34 | return this
35 | }
36 | useWithVerb(verb, handler) {
37 | verb = verb.toUpperCase()
38 | this.use(async (req, res, next) => {
39 | if (req.method === verb) {
40 | await handler(req, res, next)
41 | }
42 | })
43 | return this
44 | }
45 | async handle(request) {
46 | const responseCtx = {
47 | body: '',
48 | headers: {}
49 | }
50 | const requestCtx = Object.assign({}, request, new URL(request.url))
51 | const createNext = n => async () => {
52 | const fn = this.handlers[n]
53 | if (!fn) return
54 | let gotCalled = false
55 | const next = createNext(n + 1)
56 | await fn(requestCtx, responseCtx, () => {
57 | gotCalled = true
58 | return next()
59 | })
60 | if (!gotCalled) {
61 | return next()
62 | }
63 | }
64 | await createNext(0)()
65 | return responseCtx.response ? responseCtx.response : new Response(responseCtx.body, responseCtx)
66 | }
67 | }
68 | for (const verb of ['get', 'post', 'delete', 'put', 'options', 'head']) {
69 | Router.prototype[verb] = function(path, handler) {
70 | if (handler) this.useRouteWithVerb(verb, path, handler)
71 | else this.useWithVerb(verb, path) // when there is only 1 argument, path is handler
72 | return this
73 | }
74 | }
75 | module.exports = Router
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GDIndex
2 |
3 | 
4 |
5 | [繁體中文](README.zhtw.md)
6 | [简体中文](README.zh.md)
7 |
8 | > GDIndex is similar to [GOIndex](https://github.com/donwa/goindex).
9 | > It allows you to deploy a "Google Drive Index" on CloudFlare Workers along with many extra features
10 | >
11 | > By the way, instead of modify from GOIndex, this is a total rewrite
12 |
13 | [Demo](https://gdindex-demo.maple3142.workers.dev/)
14 |
15 | ## Difference between GOIndex and GDIndex
16 |
17 | - Frontend is based on Vue.js
18 | - Image viewer doesn't require opening new page
19 | - Video player support subtitles(Currently only srt is supported)
20 | - Online PDF, EPUB reader
21 | - No directory-level password protection(.password)
22 | - Support Http Basic Auth
23 | - Support multiple drives(personal, team) without changing server's code
24 |
25 | ## Usage
26 |
27 | ### Simple and automatic way
28 |
29 | Go [https://gdindex-code-builder.maple3142.net/](https://gdindex-code-builder.maple3142.net/), and follow its instructions.
30 |
31 | ### Manual way
32 |
33 | 1. Install [rclone](https://rclone.org/)
34 | 2. Setup your Google Drive: https://rclone.org/drive/
35 | 3. Run `rclone config file` to find your `rclone.conf` location
36 | 4. Find `refresh_token` in your `rclone.conf`, and `root_folder_id` too(optionally).
37 | 5. Copy the content of [worker/dist/worker.js](worker/dist/worker.js) to CloudFlare Workers.
38 | 6. Fill `refresh_token`, `root_folder_id` and other options on the top of the script.
39 | 7. Deploy!
40 |
41 | ### Using service accounts
42 |
43 | 1. Create a service account, a corresponding service account key, and get the JSON from the [Google Cloud Platform console](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
44 | 2. In the props object, replace the `service_account_json` value with the contents of the service account JSON file and set `service_account` to `true`.
45 | 3. Make sure that the service account in question has access to the folder specified in `root_folder_id`
46 | 4. Deploy
47 |
48 | ## Lite mode
49 |
50 | This mode will serve a simple nginx-like directory listing, and it only work with one drive. `upload` will be ignored in this mode.
51 |
52 | On the top of the script, change `lite: false` into `lite: true`, than thats all.
53 |
54 | To enable on-the-fly lite mode, especially with command-line applications, you can include a HTTP header `x-lite: true` in your requests.
55 |
56 | [Lite mode demo](https://gdindex-demo-lite.maple3142.workers.dev/)
57 |
--------------------------------------------------------------------------------
/web/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ title }}
10 |
11 |
12 |
13 |
14 |
15 | mdi-cloud {{
16 | currentDrive.text
17 | }}mdi-menu-down
18 |
19 |
20 |
21 |
26 | {{
27 | item.text
28 | }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
43 | mdi-github-circle GitHub
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
112 |
--------------------------------------------------------------------------------
/worker/xfetch.js:
--------------------------------------------------------------------------------
1 | /*
2 | * XFetch.js modified
3 | * A extremely simple fetch extension inspired by sindresorhus/ky.
4 | */
5 | const xf = (() => {
6 | const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head']
7 | class HTTPError extends Error {
8 | constructor(res) {
9 | super(res.statusText)
10 | this.name = 'HTTPError'
11 | this.response = res
12 | }
13 | }
14 | class XResponsePromise extends Promise {}
15 | for (const alias of ['arrayBuffer', 'blob', 'formData', 'json', 'text']) {
16 | // alias for .json() .text() etc...
17 | XResponsePromise.prototype[alias] = function(fn) {
18 | return this.then(res => res[alias]()).then(fn || (x => x))
19 | }
20 | }
21 | const { assign } = Object
22 | function mergeDeep(target, source) {
23 | const isObject = obj => obj && typeof obj === 'object'
24 |
25 | if (!isObject(target) || !isObject(source)) {
26 | return source
27 | }
28 |
29 | Object.keys(source).forEach(key => {
30 | const targetValue = target[key]
31 | const sourceValue = source[key]
32 |
33 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
34 | target[key] = targetValue.concat(sourceValue)
35 | } else if (isObject(targetValue) && isObject(sourceValue)) {
36 | target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue)
37 | } else {
38 | target[key] = sourceValue
39 | }
40 | })
41 |
42 | return target
43 | }
44 | const fromEntries = ent => ent.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
45 | const typeis = (...types) => val =>
46 | types.some(type => (typeof type === 'string' ? typeof val === type : val instanceof type))
47 | const isstr = typeis('string')
48 | const isobj = typeis('object')
49 | const isstrorobj = v => isstr(v) || isobj(v)
50 | const responseErrorThrower = res => {
51 | if (!res.ok) throw new HTTPError(res)
52 | return res
53 | }
54 | const extend = (defaultInit = {}) => {
55 | const xfetch = (input, init = {}) => {
56 | mergeDeep(init, defaultInit)
57 | const createQueryString = o => new init.URLSearchParams(o).toString()
58 | const parseQueryString = s => fromEntries([...new init.URLSearchParams(s).entries()])
59 | const url = new init.URL(input, init.baseURI || undefined)
60 | if (!init.headers) {
61 | init.headers = {}
62 | } else if (typeis(init.Headers)(init.headers)) {
63 | // Transform into object if it is `Headers`
64 | init.headers = fromEntries([...init.headers.entries()])
65 | }
66 | // Add json or form on body
67 | if (init.json) {
68 | init.body = JSON.stringify(init.json)
69 | init.headers['Content-Type'] = 'application/json'
70 | } else if (isstrorobj(init.urlencoded)) {
71 | init.body = isstr(init.urlencoded) ? init.urlencoded : createQueryString(init.urlencoded)
72 | init.headers['Content-Type'] = 'application/x-www-form-urlencoded'
73 | } else if (typeis(init.FormData, 'object')(init.formData)) {
74 | // init.formData is data passed by user, init.FormData is FormData constructor
75 | if (!typeis(init.FormData)(init.formData)) {
76 | const fd = new init.FormData()
77 | for (const [k, v] of Object.entries(init.formData)) {
78 | fd.append(k, v)
79 | }
80 | init.formData = fd
81 | }
82 | init.body = init.formData
83 | }
84 | // Querystring
85 | if (init.qs) {
86 | if (isstr(init.qs)) init.qs = parseQueryString(init.qs)
87 | url.search = createQueryString(assign(fromEntries([...url.searchParams.entries()]), init.qs))
88 | }
89 | return XResponsePromise.resolve(init.fetch(url, init).then(responseErrorThrower))
90 | }
91 | for (const method of METHODS) {
92 | xfetch[method] = (input, init = {}) => {
93 | init.method = method.toUpperCase()
94 | return xfetch(input, init)
95 | }
96 | }
97 | // Extra methods and classes
98 | xfetch.extend = newDefaultInit => extend(assign({}, defaultInit, newDefaultInit))
99 | xfetch.HTTPError = HTTPError
100 | return xfetch
101 | }
102 | const isWindow = typeof document !== 'undefined'
103 | const isBrowser = typeof self !== 'undefined' // works in both window & worker scope
104 | return isBrowser
105 | ? extend({
106 | fetch: fetch.bind(self),
107 | URL,
108 | Response,
109 | URLSearchParams,
110 | Headers,
111 | FormData,
112 | baseURI: isWindow ? document.baseURI : '' // since there is no document in webworkers
113 | })
114 | : extend()
115 | })()
116 | export default xf
117 |
--------------------------------------------------------------------------------
/web/src/components/FileUploadDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t('fileUpload') }}
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
52 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
179 |
187 |
--------------------------------------------------------------------------------
/web/src/xfetch.js:
--------------------------------------------------------------------------------
1 | /*
2 | * XFetch.js
3 | * A extremely simple fetch extension inspired by sindresorhus/ky.
4 | */
5 | export default (() => {
6 | const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head']
7 | class HTTPError extends Error {
8 | constructor(res) {
9 | super(res.statusText)
10 | this.name = 'HTTPError'
11 | this.response = res
12 | }
13 | }
14 | class XResponsePromise extends Promise {}
15 | for (const alias of ['arrayBuffer', 'blob', 'formData', 'json', 'text']) {
16 | // alias for .json() .text() etc...
17 | XResponsePromise.prototype[alias] = function (fn) {
18 | return this.then((res) => res[alias]()).then(fn || ((x) => x))
19 | }
20 | }
21 | function mergeDeep(target, source) {
22 | const isObject = (obj) => obj && typeof obj === 'object'
23 |
24 | if (!isObject(target) || !isObject(source)) {
25 | return source
26 | }
27 |
28 | Object.keys(source).forEach((key) => {
29 | const targetValue = target[key]
30 | const sourceValue = source[key]
31 |
32 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
33 | target[key] = targetValue.concat(sourceValue)
34 | } else if (isObject(targetValue) && isObject(sourceValue)) {
35 | target[key] = mergeDeep(
36 | Object.assign({}, targetValue),
37 | sourceValue
38 | )
39 | } else {
40 | target[key] = sourceValue
41 | }
42 | })
43 |
44 | return target
45 | }
46 | const { assign } = Object
47 | const fromEntries = (ent) =>
48 | ent.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
49 | const typeis = (...types) => (val) =>
50 | types.some((type) =>
51 | typeof type === 'string' ? typeof val === type : val instanceof type
52 | )
53 | const isstr = typeis('string')
54 | const isobj = typeis('object')
55 | const isstrorobj = (v) => isstr(v) || isobj(v)
56 | const responseErrorThrower = (res) => {
57 | if (!res.ok) throw new HTTPError(res)
58 | return res
59 | }
60 | const extend = (defaultInit = {}) => {
61 | const xfetch = (input, init = {}) => {
62 | mergeDeep(init, defaultInit)
63 | const createQueryString = (o) =>
64 | new init.URLSearchParams(o).toString()
65 | const parseQueryString = (s) =>
66 | fromEntries([...new init.URLSearchParams(s).entries()])
67 | const url = new init.URL(input, init.baseURI || undefined)
68 | if (!init.headers) {
69 | init.headers = {}
70 | } else if (typeis(init.Headers)(init.headers)) {
71 | // Transform into object if it is `Headers`
72 | init.headers = fromEntries([...init.headers.entries()])
73 | }
74 | // Add json or form on body
75 | if (init.json) {
76 | init.body = JSON.stringify(init.json)
77 | init.headers['Content-Type'] = 'application/json'
78 | } else if (isstrorobj(init.urlencoded)) {
79 | init.body = isstr(init.urlencoded)
80 | ? init.urlencoded
81 | : createQueryString(init.urlencoded)
82 | init.headers['Content-Type'] =
83 | 'application/x-www-form-urlencoded'
84 | } else if (typeis(init.FormData, 'object')(init.formData)) {
85 | // init.formData is data passed by user, init.FormData is FormData constructor
86 | if (!typeis(init.FormData)(init.formData)) {
87 | const fd = new init.FormData()
88 | for (const [k, v] of Object.entries(init.formData)) {
89 | fd.append(k, v)
90 | }
91 | init.formData = fd
92 | }
93 | init.body = init.formData
94 | }
95 | // Querystring
96 | if (init.qs) {
97 | if (isstr(init.qs)) init.qs = parseQueryString(init.qs)
98 | url.search = createQueryString(
99 | assign(
100 | fromEntries([...url.searchParams.entries()]),
101 | init.qs
102 | )
103 | )
104 | }
105 | // same-origin by default
106 | if (!init.credentials) {
107 | init.credentials = 'same-origin'
108 | }
109 | return XResponsePromise.resolve(
110 | init.fetch(url, init).then(responseErrorThrower)
111 | )
112 | }
113 | for (const method of METHODS) {
114 | xfetch[method] = (input, init = {}) => {
115 | init.method = method.toUpperCase()
116 | return xfetch(input, init)
117 | }
118 | }
119 | // Extra methods and classes
120 | xfetch.extend = (newDefaultInit) =>
121 | extend(assign({}, defaultInit, newDefaultInit))
122 | xfetch.HTTPError = HTTPError
123 | return xfetch
124 | }
125 | const isWindow = typeof document !== 'undefined'
126 | const isBrowser = typeof self !== 'undefined' // works in both window & worker scope
127 | return isBrowser
128 | ? extend({
129 | fetch: fetch.bind(self),
130 | URL,
131 | Response,
132 | URLSearchParams,
133 | Headers,
134 | FormData,
135 | baseURI: isWindow ? document.baseURI : '', // since there is no document in webworkers
136 | })
137 | : extend()
138 | })()
139 |
--------------------------------------------------------------------------------
/worker/googleDrive.js:
--------------------------------------------------------------------------------
1 | import xf from './xfetch'
2 | import { getTokenFromGCPServiceAccount } from '@sagi.io/workers-jwt'
3 |
4 | class GoogleDrive {
5 | constructor(auth) {
6 | this.auth = auth
7 | this.expires = 0
8 | this._getIdCache = new Map()
9 | }
10 | async initializeClient() {
11 | // any method that do api call must call this beforehand
12 | if (Date.now() < this.expires) return
13 |
14 | if (
15 | this.auth.service_account &&
16 | typeof this.auth.service_account_json != 'undefined'
17 | ) {
18 | const aud = this.auth.service_account_json.token_uri
19 | const serviceAccountJSON = this.auth.service_account_json
20 | const jwttoken = await getTokenFromGCPServiceAccount({
21 | serviceAccountJSON,
22 | aud,
23 | payloadAdditions: {
24 | scope: 'https://www.googleapis.com/auth/drive'
25 | }
26 | })
27 |
28 | const resp = await xf
29 | .post(serviceAccountJSON.token_uri, {
30 | urlencoded: {
31 | grant_type:
32 | 'urn:ietf:params:oauth:grant-type:jwt-bearer',
33 | assertion: jwttoken
34 | }
35 | })
36 | .json()
37 | this.client = xf.extend({
38 | baseURI: 'https://www.googleapis.com/drive/v3/',
39 | headers: {
40 | Authorization: `Bearer ${resp.access_token}`
41 | }
42 | })
43 | } else {
44 | const resp = await xf
45 | .post('https://www.googleapis.com/oauth2/v4/token', {
46 | urlencoded: {
47 | client_id: this.auth.client_id,
48 | client_secret: this.auth.client_secret,
49 | refresh_token: this.auth.refresh_token,
50 | grant_type: 'refresh_token'
51 | }
52 | })
53 | .json()
54 | this.client = xf.extend({
55 | baseURI: 'https://www.googleapis.com/drive/v3/',
56 | headers: {
57 | Authorization: `Bearer ${resp.access_token}`
58 | }
59 | })
60 | }
61 | this.expires = Date.now() + 3500 * 1000 // normally, it should expiers after 3600 seconds
62 | }
63 | async listDrive() {
64 | await this.initializeClient()
65 | return this.client.get('drives').json()
66 | }
67 | async download(id, range = '') {
68 | await this.initializeClient()
69 | return this.client.get(`files/${id}`, {
70 | qs: {
71 | includeItemsFromAllDrives: true,
72 | supportsAllDrives: true,
73 | alt: 'media'
74 | },
75 | headers: {
76 | Range: range
77 | }
78 | })
79 | }
80 | async downloadByPath(path, rootId = 'root', range = '') {
81 | const id = await this.getId(path, rootId)
82 | if (!id) return null
83 | return this.download(id, range)
84 | }
85 | async getMeta(id) {
86 | await this.initializeClient()
87 | return this.client
88 | .get(`files/${id}`, {
89 | qs: {
90 | includeItemsFromAllDrives: true,
91 | supportsAllDrives: true,
92 | fields: '*'
93 | }
94 | })
95 | .json()
96 | }
97 | async getMetaByPath(path, rootId = 'root') {
98 | const id = await this.getId(path, rootId)
99 | if (!id) return null
100 | return this.getMeta(id)
101 | }
102 | async listFolder(id) {
103 | await this.initializeClient()
104 | const getList = pageToken => {
105 | const qs = {
106 | includeItemsFromAllDrives: true,
107 | supportsAllDrives: true,
108 | q: `'${id}' in parents and trashed = false`,
109 | orderBy: 'folder,name,modifiedTime desc',
110 | fields:
111 | 'files(id,name,mimeType,size,modifiedTime),nextPageToken',
112 | pageSize: 1000
113 | }
114 | if (pageToken) {
115 | qs.pageToken = pageToken
116 | }
117 | return this.client
118 | .get('files', {
119 | qs
120 | })
121 | .json()
122 | }
123 | const files = []
124 | let pageToken
125 | do {
126 | const resp = await getList(pageToken)
127 | files.push(...resp.files)
128 | pageToken = resp.nextPageToken
129 | } while (pageToken)
130 | return { files }
131 | }
132 | async listFolderByPath(path, rootId = 'root') {
133 | const id = await this.getId(path, rootId)
134 | if (!id) return null
135 | return this.listFolder(id)
136 | }
137 | async getId(path, rootId = 'root') {
138 | const toks = path.split('/').filter(Boolean)
139 | let id = rootId
140 | for (const tok of toks) {
141 | id = await this._getId(id, tok)
142 | }
143 | return id
144 | }
145 | async _getId(parentId, childName) {
146 | if (this._getIdCache.has(parentId + childName)) {
147 | return this._getIdCache.get(parentId + childName)
148 | }
149 | await this.initializeClient()
150 | childName = childName.replace(/\'/g, `\\'`) // escape single quote
151 | const resp = await this.client
152 | .get('files', {
153 | qs: {
154 | includeItemsFromAllDrives: true,
155 | supportsAllDrives: true,
156 | q: `'${parentId}' in parents and name = '${childName}' and trashed = false`,
157 | fields: 'files(id)'
158 | }
159 | })
160 | .json()
161 | .catch(e => ({ files: [] })) // if error, make it empty
162 | if (resp.files.length === 0) {
163 | return null
164 | }
165 | this._getIdCache.has(parentId + childName)
166 | return resp.files[0].id // when there are more than 1 items, simply return the first one
167 | }
168 | async upload(parentId, name, file) {
169 | await this.initializeClient()
170 | const createResp = await this.client.post(
171 | 'https://www.googleapis.com/upload/drive/v3/files',
172 | {
173 | qs: {
174 | uploadType: 'resumable',
175 | supportsAllDrives: true
176 | },
177 | json: {
178 | name,
179 | parents: [parentId]
180 | }
181 | }
182 | )
183 | const putUrl = createResp.headers.get('Location')
184 | return this.client
185 | .put(putUrl, {
186 | body: file
187 | })
188 | .json()
189 | }
190 | async uploadByPath(path, name, file, rootId = 'root') {
191 | const id = await this.getId(path, rootId)
192 | if (!id) return null
193 | return this.upload(id, name, file)
194 | }
195 | async delete(fileId) {
196 | return this.client.delete(`files/${fileId}`)
197 | }
198 | async deleteByPath(path, rootId = 'root') {
199 | const id = await this.getId(path, rootId)
200 | if (!id) return null
201 | return this.delete(id)
202 | }
203 | }
204 | export default GoogleDrive
205 |
--------------------------------------------------------------------------------
/code-builder/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GDIndex code builder
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
GDIndex Code Builder
22 |
23 | This is a simple website to help you get started
24 | with
25 | GDIndex . The source code of this website can be found
31 | here , so no need to worry about whether it is
36 | trustworthy.
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 1. Get Authorization Code
48 |
49 |
50 | Click the following link and authorize, then
51 | copy the code you get.
52 |
53 |
Click me
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
2. Fill the form
68 |
146 |
147 |
148 |
149 |
150 |
155 |
156 |
157 |
158 |
3. Finished
159 |
160 | Copy the following code and go to
161 | CloudFlare Workers
166 | to paste the code.
167 |
168 |
172 | Copy the code to clipboard
173 |
174 |
179 |
180 |
181 |
182 |
183 |
3. Failed
184 |
185 |
186 |
187 |
188 |
189 |
190 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/worker/index.js:
--------------------------------------------------------------------------------
1 | import mime from 'mime'
2 | import GoogleDrive from './googleDrive'
3 |
4 | const gd = new GoogleDrive(self.props)
5 |
6 | const HTML = `${self.props.title}
324 |
358 |
--------------------------------------------------------------------------------
/code-builder/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | accepts@~1.3.7:
6 | version "1.3.7"
7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
8 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
9 | dependencies:
10 | mime-types "~2.1.24"
11 | negotiator "0.6.2"
12 |
13 | array-flatten@1.1.1:
14 | version "1.1.1"
15 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
16 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
17 |
18 | asynckit@^0.4.0:
19 | version "0.4.0"
20 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
21 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
22 |
23 | body-parser@1.19.0, body-parser@^1.19.0:
24 | version "1.19.0"
25 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
26 | integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
27 | dependencies:
28 | bytes "3.1.0"
29 | content-type "~1.0.4"
30 | debug "2.6.9"
31 | depd "~1.1.2"
32 | http-errors "1.7.2"
33 | iconv-lite "0.4.24"
34 | on-finished "~2.3.0"
35 | qs "6.7.0"
36 | raw-body "2.4.0"
37 | type-is "~1.6.17"
38 |
39 | bytes@3.1.0:
40 | version "3.1.0"
41 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
42 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
43 |
44 | combined-stream@^1.0.6:
45 | version "1.0.8"
46 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
47 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
48 | dependencies:
49 | delayed-stream "~1.0.0"
50 |
51 | content-disposition@0.5.3:
52 | version "0.5.3"
53 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
54 | integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
55 | dependencies:
56 | safe-buffer "5.1.2"
57 |
58 | content-type@~1.0.4:
59 | version "1.0.4"
60 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
61 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
62 |
63 | cookie-signature@1.0.6:
64 | version "1.0.6"
65 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
66 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
67 |
68 | cookie@0.4.0:
69 | version "0.4.0"
70 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
71 | integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
72 |
73 | debug@2.6.9:
74 | version "2.6.9"
75 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
76 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
77 | dependencies:
78 | ms "2.0.0"
79 |
80 | delayed-stream@~1.0.0:
81 | version "1.0.0"
82 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
83 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
84 |
85 | depd@~1.1.2:
86 | version "1.1.2"
87 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
88 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
89 |
90 | destroy@~1.0.4:
91 | version "1.0.4"
92 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
93 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
94 |
95 | ee-first@1.1.1:
96 | version "1.1.1"
97 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
98 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
99 |
100 | encodeurl@~1.0.2:
101 | version "1.0.2"
102 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
103 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
104 |
105 | escape-html@~1.0.3:
106 | version "1.0.3"
107 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
108 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
109 |
110 | etag@~1.8.1:
111 | version "1.8.1"
112 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
113 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
114 |
115 | express@^4.17.1:
116 | version "4.17.1"
117 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
118 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
119 | dependencies:
120 | accepts "~1.3.7"
121 | array-flatten "1.1.1"
122 | body-parser "1.19.0"
123 | content-disposition "0.5.3"
124 | content-type "~1.0.4"
125 | cookie "0.4.0"
126 | cookie-signature "1.0.6"
127 | debug "2.6.9"
128 | depd "~1.1.2"
129 | encodeurl "~1.0.2"
130 | escape-html "~1.0.3"
131 | etag "~1.8.1"
132 | finalhandler "~1.1.2"
133 | fresh "0.5.2"
134 | merge-descriptors "1.0.1"
135 | methods "~1.1.2"
136 | on-finished "~2.3.0"
137 | parseurl "~1.3.3"
138 | path-to-regexp "0.1.7"
139 | proxy-addr "~2.0.5"
140 | qs "6.7.0"
141 | range-parser "~1.2.1"
142 | safe-buffer "5.1.2"
143 | send "0.17.1"
144 | serve-static "1.14.1"
145 | setprototypeof "1.1.1"
146 | statuses "~1.5.0"
147 | type-is "~1.6.18"
148 | utils-merge "1.0.1"
149 | vary "~1.1.2"
150 |
151 | finalhandler@~1.1.2:
152 | version "1.1.2"
153 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
154 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
155 | dependencies:
156 | debug "2.6.9"
157 | encodeurl "~1.0.2"
158 | escape-html "~1.0.3"
159 | on-finished "~2.3.0"
160 | parseurl "~1.3.3"
161 | statuses "~1.5.0"
162 | unpipe "~1.0.0"
163 |
164 | form-data@^2.3.3:
165 | version "2.5.1"
166 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
167 | integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
168 | dependencies:
169 | asynckit "^0.4.0"
170 | combined-stream "^1.0.6"
171 | mime-types "^2.1.12"
172 |
173 | forwarded@~0.1.2:
174 | version "0.1.2"
175 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
176 | integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
177 |
178 | fresh@0.5.2:
179 | version "0.5.2"
180 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
181 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
182 |
183 | http-errors@1.7.2:
184 | version "1.7.2"
185 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
186 | integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
187 | dependencies:
188 | depd "~1.1.2"
189 | inherits "2.0.3"
190 | setprototypeof "1.1.1"
191 | statuses ">= 1.5.0 < 2"
192 | toidentifier "1.0.0"
193 |
194 | http-errors@~1.7.2:
195 | version "1.7.3"
196 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
197 | integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
198 | dependencies:
199 | depd "~1.1.2"
200 | inherits "2.0.4"
201 | setprototypeof "1.1.1"
202 | statuses ">= 1.5.0 < 2"
203 | toidentifier "1.0.0"
204 |
205 | iconv-lite@0.4.24:
206 | version "0.4.24"
207 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
208 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
209 | dependencies:
210 | safer-buffer ">= 2.1.2 < 3"
211 |
212 | inherits@2.0.3:
213 | version "2.0.3"
214 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
215 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
216 |
217 | inherits@2.0.4:
218 | version "2.0.4"
219 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
220 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
221 |
222 | ipaddr.js@1.9.1:
223 | version "1.9.1"
224 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
225 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
226 |
227 | media-typer@0.3.0:
228 | version "0.3.0"
229 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
230 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
231 |
232 | merge-descriptors@1.0.1:
233 | version "1.0.1"
234 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
235 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
236 |
237 | methods@~1.1.2:
238 | version "1.1.2"
239 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
240 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
241 |
242 | mime-db@1.44.0:
243 | version "1.44.0"
244 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
245 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
246 |
247 | mime-types@^2.1.12, mime-types@~2.1.24:
248 | version "2.1.27"
249 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
250 | integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
251 | dependencies:
252 | mime-db "1.44.0"
253 |
254 | mime@1.6.0:
255 | version "1.6.0"
256 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
257 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
258 |
259 | ms@2.0.0:
260 | version "2.0.0"
261 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
262 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
263 |
264 | ms@2.1.1:
265 | version "2.1.1"
266 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
267 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
268 |
269 | negotiator@0.6.2:
270 | version "0.6.2"
271 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
272 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
273 |
274 | node-fetch@^2.2.0:
275 | version "2.6.1"
276 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
277 | integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
278 |
279 | on-finished@~2.3.0:
280 | version "2.3.0"
281 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
282 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
283 | dependencies:
284 | ee-first "1.1.1"
285 |
286 | parseurl@~1.3.3:
287 | version "1.3.3"
288 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
289 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
290 |
291 | path-to-regexp@0.1.7:
292 | version "0.1.7"
293 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
294 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
295 |
296 | proxy-addr@~2.0.5:
297 | version "2.0.6"
298 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
299 | integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
300 | dependencies:
301 | forwarded "~0.1.2"
302 | ipaddr.js "1.9.1"
303 |
304 | qs@6.7.0:
305 | version "6.7.0"
306 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
307 | integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
308 |
309 | range-parser@~1.2.1:
310 | version "1.2.1"
311 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
312 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
313 |
314 | raw-body@2.4.0:
315 | version "2.4.0"
316 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
317 | integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
318 | dependencies:
319 | bytes "3.1.0"
320 | http-errors "1.7.2"
321 | iconv-lite "0.4.24"
322 | unpipe "1.0.0"
323 |
324 | safe-buffer@5.1.2:
325 | version "5.1.2"
326 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
327 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
328 |
329 | "safer-buffer@>= 2.1.2 < 3":
330 | version "2.1.2"
331 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
332 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
333 |
334 | send@0.17.1:
335 | version "0.17.1"
336 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
337 | integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
338 | dependencies:
339 | debug "2.6.9"
340 | depd "~1.1.2"
341 | destroy "~1.0.4"
342 | encodeurl "~1.0.2"
343 | escape-html "~1.0.3"
344 | etag "~1.8.1"
345 | fresh "0.5.2"
346 | http-errors "~1.7.2"
347 | mime "1.6.0"
348 | ms "2.1.1"
349 | on-finished "~2.3.0"
350 | range-parser "~1.2.1"
351 | statuses "~1.5.0"
352 |
353 | serve-static@1.14.1:
354 | version "1.14.1"
355 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
356 | integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
357 | dependencies:
358 | encodeurl "~1.0.2"
359 | escape-html "~1.0.3"
360 | parseurl "~1.3.3"
361 | send "0.17.1"
362 |
363 | setprototypeof@1.1.1:
364 | version "1.1.1"
365 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
366 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
367 |
368 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
369 | version "1.5.0"
370 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
371 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
372 |
373 | toidentifier@1.0.0:
374 | version "1.0.0"
375 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
376 | integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
377 |
378 | type-is@~1.6.17, type-is@~1.6.18:
379 | version "1.6.18"
380 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
381 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
382 | dependencies:
383 | media-typer "0.3.0"
384 | mime-types "~2.1.24"
385 |
386 | unpipe@1.0.0, unpipe@~1.0.0:
387 | version "1.0.0"
388 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
389 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
390 |
391 | utils-merge@1.0.1:
392 | version "1.0.1"
393 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
394 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
395 |
396 | vary@~1.1.2:
397 | version "1.1.2"
398 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
399 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
400 |
401 | xfetch-js@^0.5.0:
402 | version "0.5.0"
403 | resolved "https://registry.yarnpkg.com/xfetch-js/-/xfetch-js-0.5.0.tgz#51a72a708d3eee061479562567df9477801cbf90"
404 | integrity sha512-Wd0URa+lArdrZN7eQcm4A5ha02y3VSdfp35iyZCf+RiNZMfxNgZzvitYudYSXLfJjWkViZdtPfxEJ1ooyY4Zog==
405 | dependencies:
406 | form-data "^2.3.3"
407 | node-fetch "^2.2.0"
408 |
--------------------------------------------------------------------------------