├── 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 | 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 | 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 | ![preview](https://i.imgur.com/ENkZwCU.png) 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 | ![preview](https://i.imgur.com/ENkZwCU.png) 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 | 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 | 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 | ![preview](https://i.imgur.com/ENkZwCU.png) 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 | 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 | 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 |
69 |
70 | 73 | 79 |
80 |
81 | 84 | 91 | If you don't know what is this, just 93 | leave it alone. 95 |
96 |
97 | 103 | 106 |
107 |
108 | 112 | 118 |
119 |
120 | 124 | 130 |
131 |
132 | 138 | 141 |
142 | 145 |
146 |
147 |
148 |
149 |
150 | 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 | --------------------------------------------------------------------------------