├── 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 │ ├── main.js │ ├── components │ │ ├── PdfViewer.vue │ │ ├── EpubViewer.vue │ │ ├── LoginDialog.vue │ │ ├── VideoViewer.vue │ │ ├── FileUploadDialog.vue │ │ └── FileViewer.vue │ ├── 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 └── dist │ └── worker.js ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── custom.md │ └── bug-report-------.md ├── README.zh.md ├── README.zhtw.md ├── LICENSE ├── README.md └── .gitignore /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/coko8023/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/coko8023/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /web/dist/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coko8023/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /web/dist/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coko8023/GDIndex/master/web/dist/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /web/dist/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coko8023/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gdindex-worker", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "mime": "^2.4.4", 8 | "path-to-regexp": "^3.1.0" 9 | }, 10 | "devDependencies": { 11 | "bili": "^4.8.1" 12 | }, 13 | "scripts": { 14 | "build": "bili" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /worker/bili.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | input: 'index.js', 3 | output: { 4 | dir: 'dist', 5 | fileName: 'worker.js', 6 | format: 'iife' 7 | }, 8 | minify: false, 9 | target: 'browser', 10 | banner: ` 11 | self.props = { 12 | title: 'GDIndex', 13 | default_root_id: 'root', 14 | client_id: '202264815644.apps.googleusercontent.com', 15 | client_secret: 'X4Z3ca8xfWDb1Voo-F9a7ZxJ', 16 | refresh_token: '', 17 | auth: false, 18 | user: '', 19 | pass: '', 20 | upload: false, 21 | lite: false 22 | };`.slice(1) 23 | } 24 | -------------------------------------------------------------------------------- /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/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/src/components/PdfViewer.vue: -------------------------------------------------------------------------------- 1 | 12 | 21 | 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.glitch.me/](https://gdindex-code-builder.glitch.me/)(英文) 并遵照它的指示。 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 | -------------------------------------------------------------------------------- /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.glitch.me/](https://gdindex-code-builder.glitch.me/)(英文) 並遵照它的指示。 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 | -------------------------------------------------------------------------------- /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": "^2.6.5", 11 | "date-fns": "^2.4.1", 12 | "inline-resource": "^0.1.7", 13 | "inliner": "^1.13.1", 14 | "portal-vue": "^2.1.6", 15 | "pretty-bytes": "^5.3.0", 16 | "viewerjs": "^1.3.7", 17 | "vue": "^2.6.10", 18 | "vue-i18n": "^8.0.0", 19 | "vue-router": "^3.0.3", 20 | "vuetify": "^2.0.0", 21 | "xfetch-js": "^0.3.4" 22 | }, 23 | "devDependencies": { 24 | "@mdi/font": "^4.4.95", 25 | "@vue/cli-plugin-babel": "^3.11.0", 26 | "@vue/cli-plugin-eslint": "^3.11.0", 27 | "@vue/cli-service": "^3.11.0", 28 | "@vue/eslint-config-prettier": "^5.0.0", 29 | "babel-eslint": "^10.0.1", 30 | "eslint": "^5.16.0", 31 | "eslint-plugin-prettier": "^3.1.0", 32 | "eslint-plugin-vue": "^5.0.0", 33 | "material-design-icons-iconfont": "^5.0.1", 34 | "prettier": "^1.18.2", 35 | "raw-loader": "^3.1.0", 36 | "sass": "^1.17.4", 37 | "sass-loader": "^7.1.0", 38 | "vue-cli-plugin-i18n": "^0.6.0", 39 | "vue-cli-plugin-vuetify": "^0.6.3", 40 | "vue-template-compiler": "^2.6.10", 41 | "vuetify-loader": "^1.2.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/components/LoginDialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 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.glitch.me/](https://gdindex-code-builder.glitch.me/), 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 | ## Lite mode 42 | 43 | This mode will serve a simple nginx-like directory listing, and it only work with one drive. `upload` will be ignored in this mode. 44 | 45 | On the top of the script, change `lite: false` into `lite: true`, than thats all. 46 | 47 | [Lite mode demo](https://gdindex-demo-lite.maple3142.workers.dev/) 48 | -------------------------------------------------------------------------------- /web/src/components/VideoViewer.vue: -------------------------------------------------------------------------------- 1 | 19 | 74 | 81 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 3 | class GoogleDrive { 4 | constructor(auth) { 5 | this.auth = auth 6 | this.expires = 0 7 | this._getIdCache = new Map() 8 | } 9 | async initializeClient() { 10 | // any method that do api call must call this beforehand 11 | if (Date.now() < this.expires) return 12 | const resp = await xf 13 | .post('https://www.googleapis.com/oauth2/v4/token', { 14 | urlencoded: { 15 | client_id: this.auth.client_id, 16 | client_secret: this.auth.client_secret, 17 | refresh_token: this.auth.refresh_token, 18 | grant_type: 'refresh_token' 19 | } 20 | }) 21 | .json() 22 | this.client = xf.extend({ 23 | baseURI: 'https://www.googleapis.com/drive/v3/', 24 | headers: { 25 | Authorization: `Bearer ${resp.access_token}` 26 | } 27 | }) 28 | this.expires = Date.now() + 3500 * 1000 // normally, it should expiers after 3600 seconds 29 | } 30 | async listDrive() { 31 | await this.initializeClient() 32 | return this.client.get('drives').json() 33 | } 34 | async download(id, range = '') { 35 | await this.initializeClient() 36 | return this.client.get(`files/${id}`, { 37 | qs: { 38 | includeItemsFromAllDrives: true, 39 | supportsAllDrives: true, 40 | alt: 'media' 41 | }, 42 | headers: { 43 | Range: range 44 | } 45 | }) 46 | } 47 | async downloadByPath(path, rootId = 'root', range = '') { 48 | const id = await this.getId(path, rootId) 49 | if (!id) return null 50 | return this.download(id, range) 51 | } 52 | async getMeta(id) { 53 | await this.initializeClient() 54 | return this.client 55 | .get(`files/${id}`, { 56 | qs: { 57 | includeItemsFromAllDrives: true, 58 | supportsAllDrives: true, 59 | fields: '*' 60 | } 61 | }) 62 | .json() 63 | } 64 | async getMetaByPath(path, rootId = 'root') { 65 | const id = await this.getId(path, rootId) 66 | if (!id) return null 67 | return this.getMeta(id) 68 | } 69 | async listFolder(id) { 70 | await this.initializeClient() 71 | const getList = pageToken => { 72 | const qs = { 73 | includeItemsFromAllDrives: true, 74 | supportsAllDrives: true, 75 | q: `'${id}' in parents and trashed = false`, 76 | orderBy: 'folder,name,modifiedTime desc', 77 | fields: 78 | 'files(id,name,mimeType,size,modifiedTime),nextPageToken', 79 | pageSize: 1000 80 | } 81 | if (pageToken) { 82 | qs.pageToken = pageToken 83 | } 84 | return this.client 85 | .get('files', { 86 | qs 87 | }) 88 | .json() 89 | } 90 | const files = [] 91 | let pageToken 92 | do { 93 | const resp = await getList(pageToken) 94 | files.push(...resp.files) 95 | pageToken = resp.nextPageToken 96 | } while (pageToken) 97 | return { files } 98 | } 99 | async listFolderByPath(path, rootId = 'root') { 100 | const id = await this.getId(path, rootId) 101 | if (!id) return null 102 | return this.listFolder(id) 103 | } 104 | async getId(path, rootId = 'root') { 105 | const toks = path.split('/').filter(Boolean) 106 | let id = rootId 107 | for (const tok of toks) { 108 | id = await this._getId(id, tok) 109 | } 110 | return id 111 | } 112 | async _getId(parentId, childName) { 113 | if (this._getIdCache.has(parentId + childName)) { 114 | return this._getIdCache.get(parentId + childName) 115 | } 116 | await this.initializeClient() 117 | childName = childName.replace(/\'/g, `\\'`) // escape single quote 118 | const resp = await this.client 119 | .get('files', { 120 | qs: { 121 | includeItemsFromAllDrives: true, 122 | supportsAllDrives: true, 123 | q: `'${parentId}' in parents and name = '${childName}' and trashed = false`, 124 | fields: 'files(id)' 125 | } 126 | }) 127 | .json() 128 | .catch(e => ({ files: [] })) // if error, make it empty 129 | if (resp.files.length === 0) { 130 | return null 131 | } 132 | this._getIdCache.has(parentId + childName) 133 | return resp.files[0].id // when there are more than 1 items, simply return the first one 134 | } 135 | async upload(parentId, name, file) { 136 | await this.initializeClient() 137 | const createResp = await this.client.post( 138 | 'https://www.googleapis.com/upload/drive/v3/files', 139 | { 140 | qs: { 141 | uploadType: 'resumable', 142 | supportsAllDrives: true 143 | }, 144 | json: { 145 | name, 146 | parents: [parentId] 147 | } 148 | } 149 | ) 150 | const putUrl = createResp.headers.get('Location') 151 | return this.client 152 | .put(putUrl, { 153 | body: file 154 | }) 155 | .json() 156 | } 157 | async uploadByPath(path, name, file, rootId = 'root') { 158 | const id = await this.getId(path, rootId) 159 | if (!id) return null 160 | return this.upload(id, name, file) 161 | } 162 | async delete(fileId) { 163 | return this.client.delete(`files/${fileId}`) 164 | } 165 | async deleteByPath(path, rootId = 'root') { 166 | const id = await this.getId(path, rootId) 167 | if (!id) return null 168 | return this.delete(id) 169 | } 170 | } 171 | export default GoogleDrive 172 | -------------------------------------------------------------------------------- /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} 327 | 361 | -------------------------------------------------------------------------------- /worker/dist/worker.js: -------------------------------------------------------------------------------- 1 | self.props = { 2 | title: 'GDIndex', 3 | default_root_id: 'root', 4 | client_id: '202264815644.apps.googleusercontent.com', 5 | client_secret: 'X4Z3ca8xfWDb1Voo-F9a7ZxJ', 6 | refresh_token: '', 7 | auth: false, 8 | user: '', 9 | pass: '', 10 | upload: false, 11 | lite: false 12 | }; 13 | (function () { 14 | 'use strict'; 15 | 16 | /** 17 | * @param typeMap [Object] Map of MIME type -> Array[extensions] 18 | * @param ... 19 | */ 20 | function Mime() { 21 | this._types = Object.create(null); 22 | this._extensions = Object.create(null); 23 | 24 | for (var i = 0; i < arguments.length; i++) { 25 | this.define(arguments[i]); 26 | } 27 | 28 | this.define = this.define.bind(this); 29 | this.getType = this.getType.bind(this); 30 | this.getExtension = this.getExtension.bind(this); 31 | } 32 | 33 | /** 34 | * Define mimetype -> extension mappings. Each key is a mime-type that maps 35 | * to an array of extensions associated with the type. The first extension is 36 | * used as the default extension for the type. 37 | * 38 | * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); 39 | * 40 | * If a type declares an extension that has already been defined, an error will 41 | * be thrown. To suppress this error and force the extension to be associated 42 | * with the new type, pass `force`=true. Alternatively, you may prefix the 43 | * extension with "*" to map the type to extension, without mapping the 44 | * extension to the type. 45 | * 46 | * e.g. mime.define({'audio/wav', ['wav']}, {'audio/x-wav', ['*wav']}); 47 | * 48 | * 49 | * @param map (Object) type definitions 50 | * @param force (Boolean) if true, force overriding of existing definitions 51 | */ 52 | Mime.prototype.define = function(typeMap, force) { 53 | for (var type in typeMap) { 54 | var extensions = typeMap[type].map(function(t) {return t.toLowerCase()}); 55 | type = type.toLowerCase(); 56 | 57 | for (var i = 0; i < extensions.length; i++) { 58 | var ext = extensions[i]; 59 | 60 | // '*' prefix = not the preferred type for this extension. So fixup the 61 | // extension, and skip it. 62 | if (ext[0] == '*') { 63 | continue; 64 | } 65 | 66 | if (!force && (ext in this._types)) { 67 | throw new Error( 68 | 'Attempt to change mapping for "' + ext + 69 | '" extension from "' + this._types[ext] + '" to "' + type + 70 | '". Pass `force=true` to allow this, otherwise remove "' + ext + 71 | '" from the list of extensions for "' + type + '".' 72 | ); 73 | } 74 | 75 | this._types[ext] = type; 76 | } 77 | 78 | // Use first extension as default 79 | if (force || !this._extensions[type]) { 80 | var ext = extensions[0]; 81 | this._extensions[type] = (ext[0] != '*') ? ext : ext.substr(1); 82 | } 83 | } 84 | }; 85 | 86 | /** 87 | * Lookup a mime type based on extension 88 | */ 89 | Mime.prototype.getType = function(path) { 90 | path = String(path); 91 | var last = path.replace(/^.*[/\\]/, '').toLowerCase(); 92 | var ext = last.replace(/^.*\./, '').toLowerCase(); 93 | 94 | var hasPath = last.length < path.length; 95 | var hasDot = ext.length < last.length - 1; 96 | 97 | return (hasDot || !hasPath) && this._types[ext] || null; 98 | }; 99 | 100 | /** 101 | * Return file extension associated with a mime type 102 | */ 103 | Mime.prototype.getExtension = function(type) { 104 | type = /^\s*([^;\s]*)/.test(type) && RegExp.$1; 105 | return type && this._extensions[type.toLowerCase()] || null; 106 | }; 107 | 108 | var Mime_1 = Mime; 109 | 110 | var standard = {"application/andrew-inset":["ez"],"application/applixware":["aw"],"application/atom+xml":["atom"],"application/atomcat+xml":["atomcat"],"application/atomsvc+xml":["atomsvc"],"application/bdoc":["bdoc"],"application/ccxml+xml":["ccxml"],"application/cdmi-capability":["cdmia"],"application/cdmi-container":["cdmic"],"application/cdmi-domain":["cdmid"],"application/cdmi-object":["cdmio"],"application/cdmi-queue":["cdmiq"],"application/cu-seeme":["cu"],"application/dash+xml":["mpd"],"application/davmount+xml":["davmount"],"application/docbook+xml":["dbk"],"application/dssc+der":["dssc"],"application/dssc+xml":["xdssc"],"application/ecmascript":["ecma","es"],"application/emma+xml":["emma"],"application/epub+zip":["epub"],"application/exi":["exi"],"application/font-tdpfr":["pfr"],"application/geo+json":["geojson"],"application/gml+xml":["gml"],"application/gpx+xml":["gpx"],"application/gxf":["gxf"],"application/gzip":["gz"],"application/hjson":["hjson"],"application/hyperstudio":["stk"],"application/inkml+xml":["ink","inkml"],"application/ipfix":["ipfix"],"application/java-archive":["jar","war","ear"],"application/java-serialized-object":["ser"],"application/java-vm":["class"],"application/javascript":["js","mjs"],"application/json":["json","map"],"application/json5":["json5"],"application/jsonml+json":["jsonml"],"application/ld+json":["jsonld"],"application/lost+xml":["lostxml"],"application/mac-binhex40":["hqx"],"application/mac-compactpro":["cpt"],"application/mads+xml":["mads"],"application/manifest+json":["webmanifest"],"application/marc":["mrc"],"application/marcxml+xml":["mrcx"],"application/mathematica":["ma","nb","mb"],"application/mathml+xml":["mathml"],"application/mbox":["mbox"],"application/mediaservercontrol+xml":["mscml"],"application/metalink+xml":["metalink"],"application/metalink4+xml":["meta4"],"application/mets+xml":["mets"],"application/mods+xml":["mods"],"application/mp21":["m21","mp21"],"application/mp4":["mp4s","m4p"],"application/msword":["doc","dot"],"application/mxf":["mxf"],"application/n-quads":["nq"],"application/n-triples":["nt"],"application/octet-stream":["bin","dms","lrf","mar","so","dist","distz","pkg","bpk","dump","elc","deploy","exe","dll","deb","dmg","iso","img","msi","msp","msm","buffer"],"application/oda":["oda"],"application/oebps-package+xml":["opf"],"application/ogg":["ogx"],"application/omdoc+xml":["omdoc"],"application/onenote":["onetoc","onetoc2","onetmp","onepkg"],"application/oxps":["oxps"],"application/patch-ops-error+xml":["xer"],"application/pdf":["pdf"],"application/pgp-encrypted":["pgp"],"application/pgp-signature":["asc","sig"],"application/pics-rules":["prf"],"application/pkcs10":["p10"],"application/pkcs7-mime":["p7m","p7c"],"application/pkcs7-signature":["p7s"],"application/pkcs8":["p8"],"application/pkix-attr-cert":["ac"],"application/pkix-cert":["cer"],"application/pkix-crl":["crl"],"application/pkix-pkipath":["pkipath"],"application/pkixcmp":["pki"],"application/pls+xml":["pls"],"application/postscript":["ai","eps","ps"],"application/pskc+xml":["pskcxml"],"application/raml+yaml":["raml"],"application/rdf+xml":["rdf","owl"],"application/reginfo+xml":["rif"],"application/relax-ng-compact-syntax":["rnc"],"application/resource-lists+xml":["rl"],"application/resource-lists-diff+xml":["rld"],"application/rls-services+xml":["rs"],"application/rpki-ghostbusters":["gbr"],"application/rpki-manifest":["mft"],"application/rpki-roa":["roa"],"application/rsd+xml":["rsd"],"application/rss+xml":["rss"],"application/rtf":["rtf"],"application/sbml+xml":["sbml"],"application/scvp-cv-request":["scq"],"application/scvp-cv-response":["scs"],"application/scvp-vp-request":["spq"],"application/scvp-vp-response":["spp"],"application/sdp":["sdp"],"application/set-payment-initiation":["setpay"],"application/set-registration-initiation":["setreg"],"application/shf+xml":["shf"],"application/sieve":["siv","sieve"],"application/smil+xml":["smi","smil"],"application/sparql-query":["rq"],"application/sparql-results+xml":["srx"],"application/srgs":["gram"],"application/srgs+xml":["grxml"],"application/sru+xml":["sru"],"application/ssdl+xml":["ssdl"],"application/ssml+xml":["ssml"],"application/tei+xml":["tei","teicorpus"],"application/thraud+xml":["tfi"],"application/timestamped-data":["tsd"],"application/voicexml+xml":["vxml"],"application/wasm":["wasm"],"application/widget":["wgt"],"application/winhlp":["hlp"],"application/wsdl+xml":["wsdl"],"application/wspolicy+xml":["wspolicy"],"application/xaml+xml":["xaml"],"application/xcap-diff+xml":["xdf"],"application/xenc+xml":["xenc"],"application/xhtml+xml":["xhtml","xht"],"application/xml":["xml","xsl","xsd","rng"],"application/xml-dtd":["dtd"],"application/xop+xml":["xop"],"application/xproc+xml":["xpl"],"application/xslt+xml":["xslt"],"application/xspf+xml":["xspf"],"application/xv+xml":["mxml","xhvml","xvml","xvm"],"application/yang":["yang"],"application/yin+xml":["yin"],"application/zip":["zip"],"audio/3gpp":["*3gpp"],"audio/adpcm":["adp"],"audio/basic":["au","snd"],"audio/midi":["mid","midi","kar","rmi"],"audio/mp3":["*mp3"],"audio/mp4":["m4a","mp4a"],"audio/mpeg":["mpga","mp2","mp2a","mp3","m2a","m3a"],"audio/ogg":["oga","ogg","spx"],"audio/s3m":["s3m"],"audio/silk":["sil"],"audio/wav":["wav"],"audio/wave":["*wav"],"audio/webm":["weba"],"audio/xm":["xm"],"font/collection":["ttc"],"font/otf":["otf"],"font/ttf":["ttf"],"font/woff":["woff"],"font/woff2":["woff2"],"image/aces":["exr"],"image/apng":["apng"],"image/bmp":["bmp"],"image/cgm":["cgm"],"image/dicom-rle":["drle"],"image/emf":["emf"],"image/fits":["fits"],"image/g3fax":["g3"],"image/gif":["gif"],"image/heic":["heic"],"image/heic-sequence":["heics"],"image/heif":["heif"],"image/heif-sequence":["heifs"],"image/ief":["ief"],"image/jls":["jls"],"image/jp2":["jp2","jpg2"],"image/jpeg":["jpeg","jpg","jpe"],"image/jpm":["jpm"],"image/jpx":["jpx","jpf"],"image/jxr":["jxr"],"image/ktx":["ktx"],"image/png":["png"],"image/sgi":["sgi"],"image/svg+xml":["svg","svgz"],"image/t38":["t38"],"image/tiff":["tif","tiff"],"image/tiff-fx":["tfx"],"image/webp":["webp"],"image/wmf":["wmf"],"message/disposition-notification":["disposition-notification"],"message/global":["u8msg"],"message/global-delivery-status":["u8dsn"],"message/global-disposition-notification":["u8mdn"],"message/global-headers":["u8hdr"],"message/rfc822":["eml","mime"],"model/3mf":["3mf"],"model/gltf+json":["gltf"],"model/gltf-binary":["glb"],"model/iges":["igs","iges"],"model/mesh":["msh","mesh","silo"],"model/stl":["stl"],"model/vrml":["wrl","vrml"],"model/x3d+binary":["*x3db","x3dbz"],"model/x3d+fastinfoset":["x3db"],"model/x3d+vrml":["*x3dv","x3dvz"],"model/x3d+xml":["x3d","x3dz"],"model/x3d-vrml":["x3dv"],"text/cache-manifest":["appcache","manifest"],"text/calendar":["ics","ifb"],"text/coffeescript":["coffee","litcoffee"],"text/css":["css"],"text/csv":["csv"],"text/html":["html","htm","shtml"],"text/jade":["jade"],"text/jsx":["jsx"],"text/less":["less"],"text/markdown":["markdown","md"],"text/mathml":["mml"],"text/mdx":["mdx"],"text/n3":["n3"],"text/plain":["txt","text","conf","def","list","log","in","ini"],"text/richtext":["rtx"],"text/rtf":["*rtf"],"text/sgml":["sgml","sgm"],"text/shex":["shex"],"text/slim":["slim","slm"],"text/stylus":["stylus","styl"],"text/tab-separated-values":["tsv"],"text/troff":["t","tr","roff","man","me","ms"],"text/turtle":["ttl"],"text/uri-list":["uri","uris","urls"],"text/vcard":["vcard"],"text/vtt":["vtt"],"text/xml":["*xml"],"text/yaml":["yaml","yml"],"video/3gpp":["3gp","3gpp"],"video/3gpp2":["3g2"],"video/h261":["h261"],"video/h263":["h263"],"video/h264":["h264"],"video/jpeg":["jpgv"],"video/jpm":["*jpm","jpgm"],"video/mj2":["mj2","mjp2"],"video/mp2t":["ts"],"video/mp4":["mp4","mp4v","mpg4"],"video/mpeg":["mpeg","mpg","mpe","m1v","m2v"],"video/ogg":["ogv"],"video/quicktime":["qt","mov"],"video/webm":["webm"]}; 111 | 112 | var other = {"application/prs.cww":["cww"],"application/vnd.3gpp.pic-bw-large":["plb"],"application/vnd.3gpp.pic-bw-small":["psb"],"application/vnd.3gpp.pic-bw-var":["pvb"],"application/vnd.3gpp2.tcap":["tcap"],"application/vnd.3m.post-it-notes":["pwn"],"application/vnd.accpac.simply.aso":["aso"],"application/vnd.accpac.simply.imp":["imp"],"application/vnd.acucobol":["acu"],"application/vnd.acucorp":["atc","acutc"],"application/vnd.adobe.air-application-installer-package+zip":["air"],"application/vnd.adobe.formscentral.fcdt":["fcdt"],"application/vnd.adobe.fxp":["fxp","fxpl"],"application/vnd.adobe.xdp+xml":["xdp"],"application/vnd.adobe.xfdf":["xfdf"],"application/vnd.ahead.space":["ahead"],"application/vnd.airzip.filesecure.azf":["azf"],"application/vnd.airzip.filesecure.azs":["azs"],"application/vnd.amazon.ebook":["azw"],"application/vnd.americandynamics.acc":["acc"],"application/vnd.amiga.ami":["ami"],"application/vnd.android.package-archive":["apk"],"application/vnd.anser-web-certificate-issue-initiation":["cii"],"application/vnd.anser-web-funds-transfer-initiation":["fti"],"application/vnd.antix.game-component":["atx"],"application/vnd.apple.installer+xml":["mpkg"],"application/vnd.apple.keynote":["keynote"],"application/vnd.apple.mpegurl":["m3u8"],"application/vnd.apple.numbers":["numbers"],"application/vnd.apple.pages":["pages"],"application/vnd.apple.pkpass":["pkpass"],"application/vnd.aristanetworks.swi":["swi"],"application/vnd.astraea-software.iota":["iota"],"application/vnd.audiograph":["aep"],"application/vnd.blueice.multipass":["mpm"],"application/vnd.bmi":["bmi"],"application/vnd.businessobjects":["rep"],"application/vnd.chemdraw+xml":["cdxml"],"application/vnd.chipnuts.karaoke-mmd":["mmd"],"application/vnd.cinderella":["cdy"],"application/vnd.citationstyles.style+xml":["csl"],"application/vnd.claymore":["cla"],"application/vnd.cloanto.rp9":["rp9"],"application/vnd.clonk.c4group":["c4g","c4d","c4f","c4p","c4u"],"application/vnd.cluetrust.cartomobile-config":["c11amc"],"application/vnd.cluetrust.cartomobile-config-pkg":["c11amz"],"application/vnd.commonspace":["csp"],"application/vnd.contact.cmsg":["cdbcmsg"],"application/vnd.cosmocaller":["cmc"],"application/vnd.crick.clicker":["clkx"],"application/vnd.crick.clicker.keyboard":["clkk"],"application/vnd.crick.clicker.palette":["clkp"],"application/vnd.crick.clicker.template":["clkt"],"application/vnd.crick.clicker.wordbank":["clkw"],"application/vnd.criticaltools.wbs+xml":["wbs"],"application/vnd.ctc-posml":["pml"],"application/vnd.cups-ppd":["ppd"],"application/vnd.curl.car":["car"],"application/vnd.curl.pcurl":["pcurl"],"application/vnd.dart":["dart"],"application/vnd.data-vision.rdz":["rdz"],"application/vnd.dece.data":["uvf","uvvf","uvd","uvvd"],"application/vnd.dece.ttml+xml":["uvt","uvvt"],"application/vnd.dece.unspecified":["uvx","uvvx"],"application/vnd.dece.zip":["uvz","uvvz"],"application/vnd.denovo.fcselayout-link":["fe_launch"],"application/vnd.dna":["dna"],"application/vnd.dolby.mlp":["mlp"],"application/vnd.dpgraph":["dpg"],"application/vnd.dreamfactory":["dfac"],"application/vnd.ds-keypoint":["kpxx"],"application/vnd.dvb.ait":["ait"],"application/vnd.dvb.service":["svc"],"application/vnd.dynageo":["geo"],"application/vnd.ecowin.chart":["mag"],"application/vnd.enliven":["nml"],"application/vnd.epson.esf":["esf"],"application/vnd.epson.msf":["msf"],"application/vnd.epson.quickanime":["qam"],"application/vnd.epson.salt":["slt"],"application/vnd.epson.ssf":["ssf"],"application/vnd.eszigno3+xml":["es3","et3"],"application/vnd.ezpix-album":["ez2"],"application/vnd.ezpix-package":["ez3"],"application/vnd.fdf":["fdf"],"application/vnd.fdsn.mseed":["mseed"],"application/vnd.fdsn.seed":["seed","dataless"],"application/vnd.flographit":["gph"],"application/vnd.fluxtime.clip":["ftc"],"application/vnd.framemaker":["fm","frame","maker","book"],"application/vnd.frogans.fnc":["fnc"],"application/vnd.frogans.ltf":["ltf"],"application/vnd.fsc.weblaunch":["fsc"],"application/vnd.fujitsu.oasys":["oas"],"application/vnd.fujitsu.oasys2":["oa2"],"application/vnd.fujitsu.oasys3":["oa3"],"application/vnd.fujitsu.oasysgp":["fg5"],"application/vnd.fujitsu.oasysprs":["bh2"],"application/vnd.fujixerox.ddd":["ddd"],"application/vnd.fujixerox.docuworks":["xdw"],"application/vnd.fujixerox.docuworks.binder":["xbd"],"application/vnd.fuzzysheet":["fzs"],"application/vnd.genomatix.tuxedo":["txd"],"application/vnd.geogebra.file":["ggb"],"application/vnd.geogebra.tool":["ggt"],"application/vnd.geometry-explorer":["gex","gre"],"application/vnd.geonext":["gxt"],"application/vnd.geoplan":["g2w"],"application/vnd.geospace":["g3w"],"application/vnd.gmx":["gmx"],"application/vnd.google-apps.document":["gdoc"],"application/vnd.google-apps.presentation":["gslides"],"application/vnd.google-apps.spreadsheet":["gsheet"],"application/vnd.google-earth.kml+xml":["kml"],"application/vnd.google-earth.kmz":["kmz"],"application/vnd.grafeq":["gqf","gqs"],"application/vnd.groove-account":["gac"],"application/vnd.groove-help":["ghf"],"application/vnd.groove-identity-message":["gim"],"application/vnd.groove-injector":["grv"],"application/vnd.groove-tool-message":["gtm"],"application/vnd.groove-tool-template":["tpl"],"application/vnd.groove-vcard":["vcg"],"application/vnd.hal+xml":["hal"],"application/vnd.handheld-entertainment+xml":["zmm"],"application/vnd.hbci":["hbci"],"application/vnd.hhe.lesson-player":["les"],"application/vnd.hp-hpgl":["hpgl"],"application/vnd.hp-hpid":["hpid"],"application/vnd.hp-hps":["hps"],"application/vnd.hp-jlyt":["jlt"],"application/vnd.hp-pcl":["pcl"],"application/vnd.hp-pclxl":["pclxl"],"application/vnd.hydrostatix.sof-data":["sfd-hdstx"],"application/vnd.ibm.minipay":["mpy"],"application/vnd.ibm.modcap":["afp","listafp","list3820"],"application/vnd.ibm.rights-management":["irm"],"application/vnd.ibm.secure-container":["sc"],"application/vnd.iccprofile":["icc","icm"],"application/vnd.igloader":["igl"],"application/vnd.immervision-ivp":["ivp"],"application/vnd.immervision-ivu":["ivu"],"application/vnd.insors.igm":["igm"],"application/vnd.intercon.formnet":["xpw","xpx"],"application/vnd.intergeo":["i2g"],"application/vnd.intu.qbo":["qbo"],"application/vnd.intu.qfx":["qfx"],"application/vnd.ipunplugged.rcprofile":["rcprofile"],"application/vnd.irepository.package+xml":["irp"],"application/vnd.is-xpr":["xpr"],"application/vnd.isac.fcs":["fcs"],"application/vnd.jam":["jam"],"application/vnd.jcp.javame.midlet-rms":["rms"],"application/vnd.jisp":["jisp"],"application/vnd.joost.joda-archive":["joda"],"application/vnd.kahootz":["ktz","ktr"],"application/vnd.kde.karbon":["karbon"],"application/vnd.kde.kchart":["chrt"],"application/vnd.kde.kformula":["kfo"],"application/vnd.kde.kivio":["flw"],"application/vnd.kde.kontour":["kon"],"application/vnd.kde.kpresenter":["kpr","kpt"],"application/vnd.kde.kspread":["ksp"],"application/vnd.kde.kword":["kwd","kwt"],"application/vnd.kenameaapp":["htke"],"application/vnd.kidspiration":["kia"],"application/vnd.kinar":["kne","knp"],"application/vnd.koan":["skp","skd","skt","skm"],"application/vnd.kodak-descriptor":["sse"],"application/vnd.las.las+xml":["lasxml"],"application/vnd.llamagraphics.life-balance.desktop":["lbd"],"application/vnd.llamagraphics.life-balance.exchange+xml":["lbe"],"application/vnd.lotus-1-2-3":["123"],"application/vnd.lotus-approach":["apr"],"application/vnd.lotus-freelance":["pre"],"application/vnd.lotus-notes":["nsf"],"application/vnd.lotus-organizer":["org"],"application/vnd.lotus-screencam":["scm"],"application/vnd.lotus-wordpro":["lwp"],"application/vnd.macports.portpkg":["portpkg"],"application/vnd.mcd":["mcd"],"application/vnd.medcalcdata":["mc1"],"application/vnd.mediastation.cdkey":["cdkey"],"application/vnd.mfer":["mwf"],"application/vnd.mfmp":["mfm"],"application/vnd.micrografx.flo":["flo"],"application/vnd.micrografx.igx":["igx"],"application/vnd.mif":["mif"],"application/vnd.mobius.daf":["daf"],"application/vnd.mobius.dis":["dis"],"application/vnd.mobius.mbk":["mbk"],"application/vnd.mobius.mqy":["mqy"],"application/vnd.mobius.msl":["msl"],"application/vnd.mobius.plc":["plc"],"application/vnd.mobius.txf":["txf"],"application/vnd.mophun.application":["mpn"],"application/vnd.mophun.certificate":["mpc"],"application/vnd.mozilla.xul+xml":["xul"],"application/vnd.ms-artgalry":["cil"],"application/vnd.ms-cab-compressed":["cab"],"application/vnd.ms-excel":["xls","xlm","xla","xlc","xlt","xlw"],"application/vnd.ms-excel.addin.macroenabled.12":["xlam"],"application/vnd.ms-excel.sheet.binary.macroenabled.12":["xlsb"],"application/vnd.ms-excel.sheet.macroenabled.12":["xlsm"],"application/vnd.ms-excel.template.macroenabled.12":["xltm"],"application/vnd.ms-fontobject":["eot"],"application/vnd.ms-htmlhelp":["chm"],"application/vnd.ms-ims":["ims"],"application/vnd.ms-lrm":["lrm"],"application/vnd.ms-officetheme":["thmx"],"application/vnd.ms-outlook":["msg"],"application/vnd.ms-pki.seccat":["cat"],"application/vnd.ms-pki.stl":["*stl"],"application/vnd.ms-powerpoint":["ppt","pps","pot"],"application/vnd.ms-powerpoint.addin.macroenabled.12":["ppam"],"application/vnd.ms-powerpoint.presentation.macroenabled.12":["pptm"],"application/vnd.ms-powerpoint.slide.macroenabled.12":["sldm"],"application/vnd.ms-powerpoint.slideshow.macroenabled.12":["ppsm"],"application/vnd.ms-powerpoint.template.macroenabled.12":["potm"],"application/vnd.ms-project":["mpp","mpt"],"application/vnd.ms-word.document.macroenabled.12":["docm"],"application/vnd.ms-word.template.macroenabled.12":["dotm"],"application/vnd.ms-works":["wps","wks","wcm","wdb"],"application/vnd.ms-wpl":["wpl"],"application/vnd.ms-xpsdocument":["xps"],"application/vnd.mseq":["mseq"],"application/vnd.musician":["mus"],"application/vnd.muvee.style":["msty"],"application/vnd.mynfc":["taglet"],"application/vnd.neurolanguage.nlu":["nlu"],"application/vnd.nitf":["ntf","nitf"],"application/vnd.noblenet-directory":["nnd"],"application/vnd.noblenet-sealer":["nns"],"application/vnd.noblenet-web":["nnw"],"application/vnd.nokia.n-gage.data":["ngdat"],"application/vnd.nokia.n-gage.symbian.install":["n-gage"],"application/vnd.nokia.radio-preset":["rpst"],"application/vnd.nokia.radio-presets":["rpss"],"application/vnd.novadigm.edm":["edm"],"application/vnd.novadigm.edx":["edx"],"application/vnd.novadigm.ext":["ext"],"application/vnd.oasis.opendocument.chart":["odc"],"application/vnd.oasis.opendocument.chart-template":["otc"],"application/vnd.oasis.opendocument.database":["odb"],"application/vnd.oasis.opendocument.formula":["odf"],"application/vnd.oasis.opendocument.formula-template":["odft"],"application/vnd.oasis.opendocument.graphics":["odg"],"application/vnd.oasis.opendocument.graphics-template":["otg"],"application/vnd.oasis.opendocument.image":["odi"],"application/vnd.oasis.opendocument.image-template":["oti"],"application/vnd.oasis.opendocument.presentation":["odp"],"application/vnd.oasis.opendocument.presentation-template":["otp"],"application/vnd.oasis.opendocument.spreadsheet":["ods"],"application/vnd.oasis.opendocument.spreadsheet-template":["ots"],"application/vnd.oasis.opendocument.text":["odt"],"application/vnd.oasis.opendocument.text-master":["odm"],"application/vnd.oasis.opendocument.text-template":["ott"],"application/vnd.oasis.opendocument.text-web":["oth"],"application/vnd.olpc-sugar":["xo"],"application/vnd.oma.dd2+xml":["dd2"],"application/vnd.openofficeorg.extension":["oxt"],"application/vnd.openxmlformats-officedocument.presentationml.presentation":["pptx"],"application/vnd.openxmlformats-officedocument.presentationml.slide":["sldx"],"application/vnd.openxmlformats-officedocument.presentationml.slideshow":["ppsx"],"application/vnd.openxmlformats-officedocument.presentationml.template":["potx"],"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":["xlsx"],"application/vnd.openxmlformats-officedocument.spreadsheetml.template":["xltx"],"application/vnd.openxmlformats-officedocument.wordprocessingml.document":["docx"],"application/vnd.openxmlformats-officedocument.wordprocessingml.template":["dotx"],"application/vnd.osgeo.mapguide.package":["mgp"],"application/vnd.osgi.dp":["dp"],"application/vnd.osgi.subsystem":["esa"],"application/vnd.palm":["pdb","pqa","oprc"],"application/vnd.pawaafile":["paw"],"application/vnd.pg.format":["str"],"application/vnd.pg.osasli":["ei6"],"application/vnd.picsel":["efif"],"application/vnd.pmi.widget":["wg"],"application/vnd.pocketlearn":["plf"],"application/vnd.powerbuilder6":["pbd"],"application/vnd.previewsystems.box":["box"],"application/vnd.proteus.magazine":["mgz"],"application/vnd.publishare-delta-tree":["qps"],"application/vnd.pvi.ptid1":["ptid"],"application/vnd.quark.quarkxpress":["qxd","qxt","qwd","qwt","qxl","qxb"],"application/vnd.realvnc.bed":["bed"],"application/vnd.recordare.musicxml":["mxl"],"application/vnd.recordare.musicxml+xml":["musicxml"],"application/vnd.rig.cryptonote":["cryptonote"],"application/vnd.rim.cod":["cod"],"application/vnd.rn-realmedia":["rm"],"application/vnd.rn-realmedia-vbr":["rmvb"],"application/vnd.route66.link66+xml":["link66"],"application/vnd.sailingtracker.track":["st"],"application/vnd.seemail":["see"],"application/vnd.sema":["sema"],"application/vnd.semd":["semd"],"application/vnd.semf":["semf"],"application/vnd.shana.informed.formdata":["ifm"],"application/vnd.shana.informed.formtemplate":["itp"],"application/vnd.shana.informed.interchange":["iif"],"application/vnd.shana.informed.package":["ipk"],"application/vnd.simtech-mindmapper":["twd","twds"],"application/vnd.smaf":["mmf"],"application/vnd.smart.teacher":["teacher"],"application/vnd.solent.sdkm+xml":["sdkm","sdkd"],"application/vnd.spotfire.dxp":["dxp"],"application/vnd.spotfire.sfs":["sfs"],"application/vnd.stardivision.calc":["sdc"],"application/vnd.stardivision.draw":["sda"],"application/vnd.stardivision.impress":["sdd"],"application/vnd.stardivision.math":["smf"],"application/vnd.stardivision.writer":["sdw","vor"],"application/vnd.stardivision.writer-global":["sgl"],"application/vnd.stepmania.package":["smzip"],"application/vnd.stepmania.stepchart":["sm"],"application/vnd.sun.wadl+xml":["wadl"],"application/vnd.sun.xml.calc":["sxc"],"application/vnd.sun.xml.calc.template":["stc"],"application/vnd.sun.xml.draw":["sxd"],"application/vnd.sun.xml.draw.template":["std"],"application/vnd.sun.xml.impress":["sxi"],"application/vnd.sun.xml.impress.template":["sti"],"application/vnd.sun.xml.math":["sxm"],"application/vnd.sun.xml.writer":["sxw"],"application/vnd.sun.xml.writer.global":["sxg"],"application/vnd.sun.xml.writer.template":["stw"],"application/vnd.sus-calendar":["sus","susp"],"application/vnd.svd":["svd"],"application/vnd.symbian.install":["sis","sisx"],"application/vnd.syncml+xml":["xsm"],"application/vnd.syncml.dm+wbxml":["bdm"],"application/vnd.syncml.dm+xml":["xdm"],"application/vnd.tao.intent-module-archive":["tao"],"application/vnd.tcpdump.pcap":["pcap","cap","dmp"],"application/vnd.tmobile-livetv":["tmo"],"application/vnd.trid.tpt":["tpt"],"application/vnd.triscape.mxs":["mxs"],"application/vnd.trueapp":["tra"],"application/vnd.ufdl":["ufd","ufdl"],"application/vnd.uiq.theme":["utz"],"application/vnd.umajin":["umj"],"application/vnd.unity":["unityweb"],"application/vnd.uoml+xml":["uoml"],"application/vnd.vcx":["vcx"],"application/vnd.visio":["vsd","vst","vss","vsw"],"application/vnd.visionary":["vis"],"application/vnd.vsf":["vsf"],"application/vnd.wap.wbxml":["wbxml"],"application/vnd.wap.wmlc":["wmlc"],"application/vnd.wap.wmlscriptc":["wmlsc"],"application/vnd.webturbo":["wtb"],"application/vnd.wolfram.player":["nbp"],"application/vnd.wordperfect":["wpd"],"application/vnd.wqd":["wqd"],"application/vnd.wt.stf":["stf"],"application/vnd.xara":["xar"],"application/vnd.xfdl":["xfdl"],"application/vnd.yamaha.hv-dic":["hvd"],"application/vnd.yamaha.hv-script":["hvs"],"application/vnd.yamaha.hv-voice":["hvp"],"application/vnd.yamaha.openscoreformat":["osf"],"application/vnd.yamaha.openscoreformat.osfpvg+xml":["osfpvg"],"application/vnd.yamaha.smaf-audio":["saf"],"application/vnd.yamaha.smaf-phrase":["spf"],"application/vnd.yellowriver-custom-menu":["cmp"],"application/vnd.zul":["zir","zirz"],"application/vnd.zzazz.deck+xml":["zaz"],"application/x-7z-compressed":["7z"],"application/x-abiword":["abw"],"application/x-ace-compressed":["ace"],"application/x-apple-diskimage":["*dmg"],"application/x-arj":["arj"],"application/x-authorware-bin":["aab","x32","u32","vox"],"application/x-authorware-map":["aam"],"application/x-authorware-seg":["aas"],"application/x-bcpio":["bcpio"],"application/x-bdoc":["*bdoc"],"application/x-bittorrent":["torrent"],"application/x-blorb":["blb","blorb"],"application/x-bzip":["bz"],"application/x-bzip2":["bz2","boz"],"application/x-cbr":["cbr","cba","cbt","cbz","cb7"],"application/x-cdlink":["vcd"],"application/x-cfs-compressed":["cfs"],"application/x-chat":["chat"],"application/x-chess-pgn":["pgn"],"application/x-chrome-extension":["crx"],"application/x-cocoa":["cco"],"application/x-conference":["nsc"],"application/x-cpio":["cpio"],"application/x-csh":["csh"],"application/x-debian-package":["*deb","udeb"],"application/x-dgc-compressed":["dgc"],"application/x-director":["dir","dcr","dxr","cst","cct","cxt","w3d","fgd","swa"],"application/x-doom":["wad"],"application/x-dtbncx+xml":["ncx"],"application/x-dtbook+xml":["dtb"],"application/x-dtbresource+xml":["res"],"application/x-dvi":["dvi"],"application/x-envoy":["evy"],"application/x-eva":["eva"],"application/x-font-bdf":["bdf"],"application/x-font-ghostscript":["gsf"],"application/x-font-linux-psf":["psf"],"application/x-font-pcf":["pcf"],"application/x-font-snf":["snf"],"application/x-font-type1":["pfa","pfb","pfm","afm"],"application/x-freearc":["arc"],"application/x-futuresplash":["spl"],"application/x-gca-compressed":["gca"],"application/x-glulx":["ulx"],"application/x-gnumeric":["gnumeric"],"application/x-gramps-xml":["gramps"],"application/x-gtar":["gtar"],"application/x-hdf":["hdf"],"application/x-httpd-php":["php"],"application/x-install-instructions":["install"],"application/x-iso9660-image":["*iso"],"application/x-java-archive-diff":["jardiff"],"application/x-java-jnlp-file":["jnlp"],"application/x-latex":["latex"],"application/x-lua-bytecode":["luac"],"application/x-lzh-compressed":["lzh","lha"],"application/x-makeself":["run"],"application/x-mie":["mie"],"application/x-mobipocket-ebook":["prc","mobi"],"application/x-ms-application":["application"],"application/x-ms-shortcut":["lnk"],"application/x-ms-wmd":["wmd"],"application/x-ms-wmz":["wmz"],"application/x-ms-xbap":["xbap"],"application/x-msaccess":["mdb"],"application/x-msbinder":["obd"],"application/x-mscardfile":["crd"],"application/x-msclip":["clp"],"application/x-msdos-program":["*exe"],"application/x-msdownload":["*exe","*dll","com","bat","*msi"],"application/x-msmediaview":["mvb","m13","m14"],"application/x-msmetafile":["*wmf","*wmz","*emf","emz"],"application/x-msmoney":["mny"],"application/x-mspublisher":["pub"],"application/x-msschedule":["scd"],"application/x-msterminal":["trm"],"application/x-mswrite":["wri"],"application/x-netcdf":["nc","cdf"],"application/x-ns-proxy-autoconfig":["pac"],"application/x-nzb":["nzb"],"application/x-perl":["pl","pm"],"application/x-pilot":["*prc","*pdb"],"application/x-pkcs12":["p12","pfx"],"application/x-pkcs7-certificates":["p7b","spc"],"application/x-pkcs7-certreqresp":["p7r"],"application/x-rar-compressed":["rar"],"application/x-redhat-package-manager":["rpm"],"application/x-research-info-systems":["ris"],"application/x-sea":["sea"],"application/x-sh":["sh"],"application/x-shar":["shar"],"application/x-shockwave-flash":["swf"],"application/x-silverlight-app":["xap"],"application/x-sql":["sql"],"application/x-stuffit":["sit"],"application/x-stuffitx":["sitx"],"application/x-subrip":["srt"],"application/x-sv4cpio":["sv4cpio"],"application/x-sv4crc":["sv4crc"],"application/x-t3vm-image":["t3"],"application/x-tads":["gam"],"application/x-tar":["tar"],"application/x-tcl":["tcl","tk"],"application/x-tex":["tex"],"application/x-tex-tfm":["tfm"],"application/x-texinfo":["texinfo","texi"],"application/x-tgif":["obj"],"application/x-ustar":["ustar"],"application/x-virtualbox-hdd":["hdd"],"application/x-virtualbox-ova":["ova"],"application/x-virtualbox-ovf":["ovf"],"application/x-virtualbox-vbox":["vbox"],"application/x-virtualbox-vbox-extpack":["vbox-extpack"],"application/x-virtualbox-vdi":["vdi"],"application/x-virtualbox-vhd":["vhd"],"application/x-virtualbox-vmdk":["vmdk"],"application/x-wais-source":["src"],"application/x-web-app-manifest+json":["webapp"],"application/x-x509-ca-cert":["der","crt","pem"],"application/x-xfig":["fig"],"application/x-xliff+xml":["xlf"],"application/x-xpinstall":["xpi"],"application/x-xz":["xz"],"application/x-zmachine":["z1","z2","z3","z4","z5","z6","z7","z8"],"audio/vnd.dece.audio":["uva","uvva"],"audio/vnd.digital-winds":["eol"],"audio/vnd.dra":["dra"],"audio/vnd.dts":["dts"],"audio/vnd.dts.hd":["dtshd"],"audio/vnd.lucent.voice":["lvp"],"audio/vnd.ms-playready.media.pya":["pya"],"audio/vnd.nuera.ecelp4800":["ecelp4800"],"audio/vnd.nuera.ecelp7470":["ecelp7470"],"audio/vnd.nuera.ecelp9600":["ecelp9600"],"audio/vnd.rip":["rip"],"audio/x-aac":["aac"],"audio/x-aiff":["aif","aiff","aifc"],"audio/x-caf":["caf"],"audio/x-flac":["flac"],"audio/x-m4a":["*m4a"],"audio/x-matroska":["mka"],"audio/x-mpegurl":["m3u"],"audio/x-ms-wax":["wax"],"audio/x-ms-wma":["wma"],"audio/x-pn-realaudio":["ram","ra"],"audio/x-pn-realaudio-plugin":["rmp"],"audio/x-realaudio":["*ra"],"audio/x-wav":["*wav"],"chemical/x-cdx":["cdx"],"chemical/x-cif":["cif"],"chemical/x-cmdf":["cmdf"],"chemical/x-cml":["cml"],"chemical/x-csml":["csml"],"chemical/x-xyz":["xyz"],"image/prs.btif":["btif"],"image/prs.pti":["pti"],"image/vnd.adobe.photoshop":["psd"],"image/vnd.airzip.accelerator.azv":["azv"],"image/vnd.dece.graphic":["uvi","uvvi","uvg","uvvg"],"image/vnd.djvu":["djvu","djv"],"image/vnd.dvb.subtitle":["*sub"],"image/vnd.dwg":["dwg"],"image/vnd.dxf":["dxf"],"image/vnd.fastbidsheet":["fbs"],"image/vnd.fpx":["fpx"],"image/vnd.fst":["fst"],"image/vnd.fujixerox.edmics-mmr":["mmr"],"image/vnd.fujixerox.edmics-rlc":["rlc"],"image/vnd.microsoft.icon":["ico"],"image/vnd.ms-modi":["mdi"],"image/vnd.ms-photo":["wdp"],"image/vnd.net-fpx":["npx"],"image/vnd.tencent.tap":["tap"],"image/vnd.valve.source.texture":["vtf"],"image/vnd.wap.wbmp":["wbmp"],"image/vnd.xiff":["xif"],"image/vnd.zbrush.pcx":["pcx"],"image/x-3ds":["3ds"],"image/x-cmu-raster":["ras"],"image/x-cmx":["cmx"],"image/x-freehand":["fh","fhc","fh4","fh5","fh7"],"image/x-icon":["*ico"],"image/x-jng":["jng"],"image/x-mrsid-image":["sid"],"image/x-ms-bmp":["*bmp"],"image/x-pcx":["*pcx"],"image/x-pict":["pic","pct"],"image/x-portable-anymap":["pnm"],"image/x-portable-bitmap":["pbm"],"image/x-portable-graymap":["pgm"],"image/x-portable-pixmap":["ppm"],"image/x-rgb":["rgb"],"image/x-tga":["tga"],"image/x-xbitmap":["xbm"],"image/x-xpixmap":["xpm"],"image/x-xwindowdump":["xwd"],"message/vnd.wfa.wsc":["wsc"],"model/vnd.collada+xml":["dae"],"model/vnd.dwf":["dwf"],"model/vnd.gdl":["gdl"],"model/vnd.gtw":["gtw"],"model/vnd.mts":["mts"],"model/vnd.opengex":["ogex"],"model/vnd.parasolid.transmit.binary":["x_b"],"model/vnd.parasolid.transmit.text":["x_t"],"model/vnd.usdz+zip":["usdz"],"model/vnd.valve.source.compiled-map":["bsp"],"model/vnd.vtu":["vtu"],"text/prs.lines.tag":["dsc"],"text/vnd.curl":["curl"],"text/vnd.curl.dcurl":["dcurl"],"text/vnd.curl.mcurl":["mcurl"],"text/vnd.curl.scurl":["scurl"],"text/vnd.dvb.subtitle":["sub"],"text/vnd.fly":["fly"],"text/vnd.fmi.flexstor":["flx"],"text/vnd.graphviz":["gv"],"text/vnd.in3d.3dml":["3dml"],"text/vnd.in3d.spot":["spot"],"text/vnd.sun.j2me.app-descriptor":["jad"],"text/vnd.wap.wml":["wml"],"text/vnd.wap.wmlscript":["wmls"],"text/x-asm":["s","asm"],"text/x-c":["c","cc","cxx","cpp","h","hh","dic"],"text/x-component":["htc"],"text/x-fortran":["f","for","f77","f90"],"text/x-handlebars-template":["hbs"],"text/x-java-source":["java"],"text/x-lua":["lua"],"text/x-markdown":["mkd"],"text/x-nfo":["nfo"],"text/x-opml":["opml"],"text/x-org":["*org"],"text/x-pascal":["p","pas"],"text/x-processing":["pde"],"text/x-sass":["sass"],"text/x-scss":["scss"],"text/x-setext":["etx"],"text/x-sfv":["sfv"],"text/x-suse-ymp":["ymp"],"text/x-uuencode":["uu"],"text/x-vcalendar":["vcs"],"text/x-vcard":["vcf"],"video/vnd.dece.hd":["uvh","uvvh"],"video/vnd.dece.mobile":["uvm","uvvm"],"video/vnd.dece.pd":["uvp","uvvp"],"video/vnd.dece.sd":["uvs","uvvs"],"video/vnd.dece.video":["uvv","uvvv"],"video/vnd.dvb.file":["dvb"],"video/vnd.fvt":["fvt"],"video/vnd.mpegurl":["mxu","m4u"],"video/vnd.ms-playready.media.pyv":["pyv"],"video/vnd.uvvu.mp4":["uvu","uvvu"],"video/vnd.vivo":["viv"],"video/x-f4v":["f4v"],"video/x-fli":["fli"],"video/x-flv":["flv"],"video/x-m4v":["m4v"],"video/x-matroska":["mkv","mk3d","mks"],"video/x-mng":["mng"],"video/x-ms-asf":["asf","asx"],"video/x-ms-vob":["vob"],"video/x-ms-wm":["wm"],"video/x-ms-wmv":["wmv"],"video/x-ms-wmx":["wmx"],"video/x-ms-wvx":["wvx"],"video/x-msvideo":["avi"],"video/x-sgi-movie":["movie"],"video/x-smv":["smv"],"x-conference/x-cooltalk":["ice"]}; 113 | 114 | var mime = new Mime_1(standard, other); 115 | 116 | /* 117 | * XFetch.js modified 118 | * A extremely simple fetch extension inspired by sindresorhus/ky. 119 | */ 120 | const xf = (() => { 121 | const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head']; 122 | 123 | class HTTPError extends Error { 124 | constructor(res) { 125 | super(res.statusText); 126 | this.name = 'HTTPError'; 127 | this.response = res; 128 | } 129 | 130 | } 131 | 132 | class XResponsePromise extends Promise {} 133 | 134 | for (const alias of ['arrayBuffer', 'blob', 'formData', 'json', 'text']) { 135 | // alias for .json() .text() etc... 136 | XResponsePromise.prototype[alias] = function (fn) { 137 | return this.then(res => res[alias]()).then(fn || (x => x)); 138 | }; 139 | } 140 | 141 | const { 142 | assign 143 | } = Object; 144 | 145 | function mergeDeep(target, source) { 146 | const isObject = obj => obj && typeof obj === 'object'; 147 | 148 | if (!isObject(target) || !isObject(source)) { 149 | return source; 150 | } 151 | 152 | Object.keys(source).forEach(key => { 153 | const targetValue = target[key]; 154 | const sourceValue = source[key]; 155 | 156 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { 157 | target[key] = targetValue.concat(sourceValue); 158 | } else if (isObject(targetValue) && isObject(sourceValue)) { 159 | target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue); 160 | } else { 161 | target[key] = sourceValue; 162 | } 163 | }); 164 | return target; 165 | } 166 | 167 | const fromEntries = ent => ent.reduce((acc, [k, v]) => (acc[k] = v, acc), {}); 168 | 169 | const typeis = (...types) => val => types.some(type => typeof type === 'string' ? typeof val === type : val instanceof type); 170 | 171 | const isstr = typeis('string'); 172 | const isobj = typeis('object'); 173 | 174 | const isstrorobj = v => isstr(v) || isobj(v); 175 | 176 | const responseErrorThrower = res => { 177 | if (!res.ok) throw new HTTPError(res); 178 | return res; 179 | }; 180 | 181 | const extend = (defaultInit = {}) => { 182 | const xfetch = (input, init = {}) => { 183 | mergeDeep(init, defaultInit); 184 | 185 | const createQueryString = o => new init.URLSearchParams(o).toString(); 186 | 187 | const parseQueryString = s => fromEntries([...new init.URLSearchParams(s).entries()]); 188 | 189 | const url = new init.URL(input, init.baseURI || undefined); 190 | 191 | if (!init.headers) { 192 | init.headers = {}; 193 | } else if (typeis(init.Headers)(init.headers)) { 194 | // Transform into object if it is `Headers` 195 | init.headers = fromEntries([...init.headers.entries()]); 196 | } // Add json or form on body 197 | 198 | 199 | if (init.json) { 200 | init.body = JSON.stringify(init.json); 201 | init.headers['Content-Type'] = 'application/json'; 202 | } else if (isstrorobj(init.urlencoded)) { 203 | init.body = isstr(init.urlencoded) ? init.urlencoded : createQueryString(init.urlencoded); 204 | init.headers['Content-Type'] = 'application/x-www-form-urlencoded'; 205 | } else if (typeis(init.FormData, 'object')(init.formData)) { 206 | // init.formData is data passed by user, init.FormData is FormData constructor 207 | if (!typeis(init.FormData)(init.formData)) { 208 | const fd = new init.FormData(); 209 | 210 | for (const [k, v] of Object.entries(init.formData)) { 211 | fd.append(k, v); 212 | } 213 | 214 | init.formData = fd; 215 | } 216 | 217 | init.body = init.formData; 218 | } // Querystring 219 | 220 | 221 | if (init.qs) { 222 | if (isstr(init.qs)) init.qs = parseQueryString(init.qs); 223 | url.search = createQueryString(assign(fromEntries([...url.searchParams.entries()]), init.qs)); 224 | } 225 | 226 | return XResponsePromise.resolve(init.fetch(url, init).then(responseErrorThrower)); 227 | }; 228 | 229 | for (const method of METHODS) { 230 | xfetch[method] = (input, init = {}) => { 231 | init.method = method.toUpperCase(); 232 | return xfetch(input, init); 233 | }; 234 | } // Extra methods and classes 235 | 236 | 237 | xfetch.extend = newDefaultInit => extend(assign({}, defaultInit, newDefaultInit)); 238 | 239 | xfetch.HTTPError = HTTPError; 240 | return xfetch; 241 | }; 242 | 243 | const isWindow = typeof document !== 'undefined'; 244 | const isBrowser = typeof self !== 'undefined'; // works in both window & worker scope 245 | 246 | return isBrowser ? extend({ 247 | fetch: fetch.bind(self), 248 | URL, 249 | Response, 250 | URLSearchParams, 251 | Headers, 252 | FormData, 253 | baseURI: isWindow ? document.baseURI : '' // since there is no document in webworkers 254 | 255 | }) : extend(); 256 | })(); 257 | 258 | class GoogleDrive { 259 | constructor(auth) { 260 | this.auth = auth; 261 | this.expires = 0; 262 | this._getIdCache = new Map(); 263 | } 264 | 265 | async initializeClient() { 266 | // any method that do api call must call this beforehand 267 | if (Date.now() < this.expires) return; 268 | const resp = await xf.post('https://www.googleapis.com/oauth2/v4/token', { 269 | urlencoded: { 270 | client_id: this.auth.client_id, 271 | client_secret: this.auth.client_secret, 272 | refresh_token: this.auth.refresh_token, 273 | grant_type: 'refresh_token' 274 | } 275 | }).json(); 276 | this.client = xf.extend({ 277 | baseURI: 'https://www.googleapis.com/drive/v3/', 278 | headers: { 279 | Authorization: `Bearer ${resp.access_token}` 280 | } 281 | }); 282 | this.expires = Date.now() + 3500 * 1000; // normally, it should expiers after 3600 seconds 283 | } 284 | 285 | async listDrive() { 286 | await this.initializeClient(); 287 | return this.client.get('drives').json(); 288 | } 289 | 290 | async download(id, range = '') { 291 | await this.initializeClient(); 292 | return this.client.get(`files/${id}`, { 293 | qs: { 294 | includeItemsFromAllDrives: true, 295 | supportsAllDrives: true, 296 | alt: 'media' 297 | }, 298 | headers: { 299 | Range: range 300 | } 301 | }); 302 | } 303 | 304 | async downloadByPath(path, rootId = 'root', range = '') { 305 | const id = await this.getId(path, rootId); 306 | if (!id) return null; 307 | return this.download(id, range); 308 | } 309 | 310 | async getMeta(id) { 311 | await this.initializeClient(); 312 | return this.client.get(`files/${id}`, { 313 | qs: { 314 | includeItemsFromAllDrives: true, 315 | supportsAllDrives: true, 316 | fields: '*' 317 | } 318 | }).json(); 319 | } 320 | 321 | async getMetaByPath(path, rootId = 'root') { 322 | const id = await this.getId(path, rootId); 323 | if (!id) return null; 324 | return this.getMeta(id); 325 | } 326 | 327 | async listFolder(id) { 328 | await this.initializeClient(); 329 | 330 | const getList = pageToken => { 331 | const qs = { 332 | includeItemsFromAllDrives: true, 333 | supportsAllDrives: true, 334 | q: `'${id}' in parents and trashed = false`, 335 | orderBy: 'folder,name,modifiedTime desc', 336 | fields: 'files(id,name,mimeType,size,modifiedTime),nextPageToken', 337 | pageSize: 1000 338 | }; 339 | 340 | if (pageToken) { 341 | qs.pageToken = pageToken; 342 | } 343 | 344 | return this.client.get('files', { 345 | qs 346 | }).json(); 347 | }; 348 | 349 | const files = []; 350 | let pageToken; 351 | 352 | do { 353 | const resp = await getList(pageToken); 354 | files.push(...resp.files); 355 | pageToken = resp.nextPageToken; 356 | } while (pageToken); 357 | 358 | return { 359 | files 360 | }; 361 | } 362 | 363 | async listFolderByPath(path, rootId = 'root') { 364 | const id = await this.getId(path, rootId); 365 | if (!id) return null; 366 | return this.listFolder(id); 367 | } 368 | 369 | async getId(path, rootId = 'root') { 370 | const toks = path.split('/').filter(Boolean); 371 | let id = rootId; 372 | 373 | for (const tok of toks) { 374 | id = await this._getId(id, tok); 375 | } 376 | 377 | return id; 378 | } 379 | 380 | async _getId(parentId, childName) { 381 | if (this._getIdCache.has(parentId + childName)) { 382 | return this._getIdCache.get(parentId + childName); 383 | } 384 | 385 | await this.initializeClient(); 386 | childName = childName.replace(/\'/g, `\\'`); // escape single quote 387 | 388 | const resp = await this.client.get('files', { 389 | qs: { 390 | includeItemsFromAllDrives: true, 391 | supportsAllDrives: true, 392 | q: `'${parentId}' in parents and name = '${childName}' and trashed = false`, 393 | fields: 'files(id)' 394 | } 395 | }).json().catch(e => ({ 396 | files: [] 397 | })); // if error, make it empty 398 | 399 | if (resp.files.length === 0) { 400 | return null; 401 | } 402 | 403 | this._getIdCache.has(parentId + childName); 404 | 405 | return resp.files[0].id; // when there are more than 1 items, simply return the first one 406 | } 407 | 408 | async upload(parentId, name, file) { 409 | await this.initializeClient(); 410 | const createResp = await this.client.post('https://www.googleapis.com/upload/drive/v3/files', { 411 | qs: { 412 | uploadType: 'resumable', 413 | supportsAllDrives: true 414 | }, 415 | json: { 416 | name, 417 | parents: [parentId] 418 | } 419 | }); 420 | const putUrl = createResp.headers.get('Location'); 421 | return this.client.put(putUrl, { 422 | body: file 423 | }).json(); 424 | } 425 | 426 | async uploadByPath(path, name, file, rootId = 'root') { 427 | const id = await this.getId(path, rootId); 428 | if (!id) return null; 429 | return this.upload(id, name, file); 430 | } 431 | 432 | async delete(fileId) { 433 | return this.client.delete(`files/${fileId}`); 434 | } 435 | 436 | async deleteByPath(path, rootId = 'root') { 437 | const id = await this.getId(path, rootId); 438 | if (!id) return null; 439 | return this.delete(id); 440 | } 441 | 442 | } 443 | 444 | const gd = new GoogleDrive(self.props); 445 | const HTML = `${self.props.title}