├── .eslintrc ├── .github └── workflows │ ├── main.yml │ └── npm-publish.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── lib ├── app.js ├── conf │ └── sys-config.js ├── core │ ├── app.js │ └── op.js ├── middleware │ ├── access-control.js │ ├── admin.js │ ├── cache-control.js │ ├── header.js │ ├── invoke-module.js │ ├── page-control.js │ └── template.js ├── routers │ ├── alidrive.js │ ├── coding.js │ ├── googledrive.js │ ├── node_fs.js │ ├── onedrive.js │ ├── phony.js │ └── teambition.js ├── starters │ ├── cf-worker.js │ ├── local-test.js │ ├── node-http.js │ ├── tencent-scf.js │ └── vercel-zeit.js ├── utils │ ├── crypto-js-aes.js │ ├── error-message.js │ ├── fetchAdapter.js │ ├── logger.js │ ├── mime.js │ ├── node.js │ ├── simple-router.js │ ├── tiny-request │ │ ├── browser-fetch.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── node-http.js │ ├── tiny-sha1.js │ └── view-helper.js └── views │ ├── art-runtime.js │ ├── art │ ├── simple.art │ └── w.w.art │ ├── art2js.js │ └── js │ ├── simple.art.js │ ├── w.w.art.js │ └── w.w.js ├── ncc ├── build.js ├── ncc_cf-worker.js ├── ncc_node-http.js ├── ncc_tencent-scf.js └── ncc_vercel-zeit.js ├── package.json └── worker └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:prettier/recommended" 5 | ], 6 | "plugins": [ 7 | "prettier" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module" 12 | }, 13 | "env": { 14 | "node": true, 15 | "es6": true 16 | }, 17 | "rules": { 18 | "array-callback-return": 2, 19 | "arrow-body-style": 2, 20 | "arrow-parens": 1, 21 | "arrow-spacing": 2, 22 | "block-scoped-var": 2, 23 | "comma-spacing": 2, 24 | "comma-style": 2, 25 | "curly": 2, 26 | "dot-notation": 2, 27 | "eqeqeq": 2, 28 | "func-call-spacing": 2, 29 | "keyword-spacing": 2, 30 | "linebreak-style": 2, 31 | "lines-around-comment": 2, 32 | "no-await-in-loop": 2, 33 | "no-console": 1, 34 | "no-control-regex": 0, 35 | "no-duplicate-imports": 2, 36 | "no-eval": 2, 37 | "no-extend-native": 2, 38 | "no-extra-label": 2, 39 | "no-global-assign": 2, 40 | "no-implicit-globals": 2, 41 | "no-labels": 2, 42 | "no-multi-str": 2, 43 | "no-multiple-empty-lines": 2, 44 | "no-prototype-builtins": 0, 45 | "no-restricted-imports": 2, 46 | "no-trailing-spaces": 2, 47 | "no-unsafe-negation": 2, 48 | "no-unused-vars": 1, 49 | "no-useless-computed-key": 1, 50 | "no-useless-rename": 1, 51 | "no-var": 2, 52 | "prefer-const": 1, 53 | "prettier/prettier": 1, 54 | "require-atomic-updates": 0, 55 | "rest-spread-spacing": 2, 56 | "semi": 1, 57 | "space-before-blocks": 2, 58 | "space-in-parens": 2, 59 | "space-infix-ops": 2, 60 | "space-unary-ops": 2, 61 | "spaced-comment": 2, 62 | "no-undef": 1, 63 | "no-empty": 1 64 | }, 65 | "ignorePatterns": [ 66 | "**/deprecated/**", 67 | "node_modules/**", 68 | "dist/**", 69 | "tmp/**", 70 | "worker/**", 71 | "lib/utils/sys-template.js", 72 | "lib/utils/fetchAdapter.js", 73 | "lib/starters/cf-worker.js", 74 | "lib/views/", 75 | "ncc/**", 76 | "lib/utils/tiny-sha1.js" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: MAIN_DEPLOY 2 | 3 | on: 4 | push: 5 | branches: 6 | - master1 7 | paths: 8 | - 'lib/*' 9 | - 'router/*' 10 | - 'utils/*' 11 | - 'views/*' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | repository: ukuq/onepoint 22 | path: onepoint 23 | - uses: actions/setup-node@v1 # 安装nodejs,版本指定10 24 | with: 25 | node-version: '10.x' 26 | - name: read config.json from secrets 27 | env: 28 | CONFIG_JSON: ${{ secrets.CONFIG_JSON }} # secrets 提供 29 | run: 30 | echo $CONFIG_JSON > ./onepoint/config2.json 31 | - name: install configure SCF CLI and deploy 32 | env: 33 | APPID: ${{ secrets.TENCENTCLOUD_APP_ID }} # secrets 提供 34 | REGION: ap-hongkong 35 | SECRET_ID: ${{ secrets.TENCENTCLOUD_SECRET_ID }} 36 | SECRET_KEY: ${{ secrets.TENCENTCLOUD_SECRET_KEY }} 37 | run: | 38 | sudo pip install scf 39 | scf configure set --appid $APPID --region $REGION --secret-id $SECRET_ID --secret-key $SECRET_KEY 40 | cp ./onepoint/test/tencent/template.yaml ./onepoint/ 41 | cd ./onepoint 42 | npm install 43 | scf deploy -f 44 | cd .. 45 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | workflow_dispatch: 10 | inputs: 11 | name: 12 | description: 'Person to greet' 13 | required: true 14 | default: 'Mona the Octocat' 15 | home: 16 | description: 'location' 17 | required: false 18 | 19 | jobs: 20 | publish-npm: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | registry-url: https://registry.npmjs.org/ 28 | - run: echo -e "/.github/\n/docs/\n/test/" >> .npmignore 29 | - run: npm ci 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | 34 | publish-gpr: 35 | if: false 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions/setup-node@v1 40 | with: 41 | node-version: 12 42 | registry-url: https://npm.pkg.github.com/ 43 | - run: echo -e "/.github/\n/docs/\n/test/" >> .npmignore 44 | - run: npm ci 45 | - run: npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | #/tmp 5 | tmp 6 | 7 | #/docs 8 | markdown 9 | 10 | #/dist 11 | dist 12 | 13 | #lock 14 | package-lock.json 15 | 16 | # local env files 17 | .env.local 18 | .env.*.local 19 | 20 | # Log files 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | 26 | # Editor directories and files 27 | .idea 28 | .vscode 29 | *.suo 30 | *.ntvs* 31 | *.njsproj 32 | *.sln 33 | *.sw? 34 | *.iml 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .idea/ 3 | dist/ 4 | markdown/ 5 | node_modules/ 6 | tmp/ 7 | worker/ 8 | ncc/ 9 | 10 | lib/deprecated/ 11 | 12 | package.json 13 | 14 | lib/views/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ukuq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OnePoint 2 | 3 | 一个轻量级、多平台、多网盘的文件目录索引(和管理)工具。 4 | 5 | 项目地址:https://github.com/ukuq/onepoint 6 | 7 | ## 项目特点 8 | 9 | 轻量级、多平台、多网盘 10 | 11 | ## 支持云盘 12 | 13 | - onedrive 14 | 15 | 官网:https://office.com/ 16 | 17 | 类型比较多,为了统一全部放置到了本模块里面,包括国际版、世纪互联版、分享链接三大类,可按照配置时的提示完成填写 18 | 19 | - googledrive 20 | 21 | 官网:http://drive.google.com/ 22 | 23 | 受限于api,所有的下载都将会由本机中转完成 24 | 25 | - coding 26 | 27 | 官网:https://coding.net/ 28 | 29 | 公开api功能太少,所有的功能都是根据cookie完成 30 | 31 | - teambition 32 | 33 | 官网:https://teambition.com/ 34 | 35 | 无公开api,所有功能通过cookie实现,cookie并不一定稳定,这一部分未实现文件管理功能 36 | 37 | - node_fs 38 | 39 | 官网:http://nodejs.org/ 40 | 41 | 基于nodejs自身fs api完成,仅用于挂载本机文件 42 | 43 | - alidrive 44 | 45 | 官网:https://www.aliyundrive.com/drive/ 46 | 47 | 通过refresh_token访问 48 | 49 | ## 快速部署 50 | 51 | ### github 测试版(2.0.0) 52 | 53 | ~~~ 54 | git clone https://github.com/ukuq/onepoint.git 55 | cd onepoint && npm install 56 | 57 | npm start 58 | # pm2 lib/starters/node-http.js 59 | ~~~ 60 | 61 | ## cloudflare 部署 62 | 63 | 参考:worker/README.md 64 | 65 | ## Demo 66 | 67 | https://onepoint.onesrc.workers.dev/ 68 | 69 | ## 更新说明 70 | 71 | ### 210620 72 | 73 | zero dependencies,零依赖 74 | 75 | 去除了 axios、cookie 依赖,项目可使用 ncc 工具打包、提高了易读性、简洁性 76 | 77 | 首次安装时不再需要输入密码登录,可以自定义设置用户名、salt和密码 78 | 79 | cloudflare worker 项目打包工具改用 ncc 完成 80 | 81 | ### 210425 82 | 83 | 新增阿里云盘,支持翻页、id 84 | 85 | 优化了 onedrive 模块,删除了 code 功能,只保留 refresh_token和share_url 86 | 87 | 优化了 googledrive 模块,删除了 code 功能,只保留 refresh_token,支持自定义 client api 88 | 89 | 删除了 art-template,改用 art 编译后的 js 文件生成 html 90 | 91 | 删除了系统分页,只保留云盘模块自身的分页功能 92 | 93 | 修复了因缓存而引起的文件下载链接过期的 bug 94 | 95 | 优化了 w.w 主题,看起来更和谐了,感谢 naicfeng 提供的demo 96 | 97 | ### 210413 98 | 99 | 增加了乐观锁,修改配置时有效,防止多次修改 100 | 101 | 重写管理页面前端代码,支持了多图片、多音频预览功能, 非常建议更新~ 102 | 103 | ## Thanks 104 | 105 | [oneindex](https://github.com/donwa/oneindex) 106 | [OneManager](https://github.com/qkqpttgf/OneManager-php) 107 | 108 | ## License 109 | 110 | MIT 111 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | const op = require('./core/op'); 2 | const app = require('./core/app'); 3 | 4 | const { MODULES, THEMES } = require('./conf/sys-config'); 5 | 6 | const { modules, themes } = op; 7 | 8 | MODULES.forEach((k) => { 9 | modules[k] = require(`./routers/${k}.js`); 10 | }); 11 | 12 | THEMES.forEach((k) => { 13 | themes[k] = require(`./views/js/${k}.js`); 14 | }); 15 | 16 | const header = require('./middleware/header'); 17 | const template = require('./middleware/template'); 18 | const admin = require('./middleware/admin'); 19 | const invokeModule = require('./middleware/invoke-module'); 20 | const logger = require('./utils/logger'); 21 | 22 | // fake koa app 23 | app.use(header); 24 | app.use(template); 25 | app.use(admin); 26 | app.use(require('./middleware/access-control')); 27 | app.use(require('./middleware/page-control')); 28 | app.use(require('./middleware/cache-control')); 29 | app.use(invokeModule); 30 | 31 | module.exports = { 32 | initialize(starter) { 33 | op.initialize(starter); 34 | }, 35 | async _handle(req) { 36 | if (op.config.version === -1) { 37 | // ? 代表未加载设置,尝试加载,若加载失败自动使用默认配置 38 | await op.readConfig(); 39 | } 40 | return app.handleRequest(req).then(({ response }) => { 41 | if (typeof response.status !== 'number' || typeof response.headers !== 'object' || typeof response.body !== 'string') { 42 | throw new Error('Internal Response Error'); 43 | } 44 | return response; 45 | }); 46 | }, 47 | async handleRequest(req) { 48 | return this._handle(req).catch((err) => { 49 | // 这一步遇到的错误一般都是请求类错误 格式错误 无法解析之类的 50 | logger.error(err); 51 | return { 52 | status: 500, 53 | headers: { 54 | 'access-control-allow-origin': '*', 55 | 'content-type': 'application/json', 56 | }, 57 | body: JSON.stringify({ error: 'InternalError', message: err.message }), 58 | }; 59 | }); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /lib/conf/sys-config.js: -------------------------------------------------------------------------------- 1 | // 系统配置参数,运行时只读 2 | const config = { 3 | PORT: 8020, 4 | NODE_ENV: 'production', 5 | 6 | SIGN_DELIMITER: '.', 7 | PAGE_SIZE: 200, 8 | 9 | ID_SIGN_LEN: 7, 10 | PAGE_SIGN_LEN: 7, 11 | 12 | CACHE_TIME_FILE: 15 * 60 * 1000, 13 | CACHE_TIME_LIST: 30 * 60 * 1000, 14 | 15 | AC_PASS_FILE: '.password=', 16 | 17 | PATH_ADMIN: '/admin/', 18 | PATH_API: '/api/', 19 | PATH_DOWN: '/down:', 20 | PATH_SHARE: '/s/', 21 | 22 | THEMES: ['w.w.art', 'simple.art'], 23 | MODULES: ['node_fs', 'onedrive', 'coding', 'teambition', 'googledrive', 'alidrive', 'phony'], 24 | }; 25 | 26 | const configTemplate = { 27 | html: '
这是一段自定义html,你也可以在这里放置一些脚本文件
', 28 | logo: 'https://cdn.onesrc.cn/uploads/images/onepoint_30.png', 29 | name: 'DemoSite', 30 | readme: `## 部署成功 31 | 32 | 恭喜部署成功,但这并不意味着系统能使用了 33 | 34 | 接下来,你需要进入 [admin](/admin/) 页面,完成一些必须的配置 35 | 36 | 要注意,某些平台的配置参数,可能需要你在平台上自行配置 37 | 38 | 配置完成后,就可以添加云盘了 39 | `, 40 | cors: ['*'], 41 | proxy: [], 42 | theme: config.THEMES[0], 43 | share_aeskey: 'password_len==16', 44 | }; 45 | config.configTemplate = configTemplate; 46 | 47 | const { _P } = require('../utils/node'); 48 | 49 | const commonSParams = [ 50 | _P('theme', '', '', 8, config.THEMES, false, false), 51 | _P('logo', '', '', 6, 'consume your own logo', false, false), 52 | _P('name', '', '', 6, 'consume your site name', false, false), 53 | _P('html', '', '', 6, 'embed html code', true, false), 54 | _P('readme', '', '', 6, 'markdown supported', true, false), 55 | _P('proxy', [], 'proxy for download', 4, '', false, false), 56 | _P('cors', [], 'Allow CORS', 4, '', false, false), 57 | _P('share_aeskey', 'password_len==16', 'share link encryption key', 4, '', true, false), 58 | ]; 59 | commonSParams.forEach((e) => { 60 | e.value = configTemplate[e.name]; 61 | }); 62 | config.commonSParams = commonSParams; 63 | 64 | const commonMParams = [ 65 | _P('path', '', '', 8, 'mount path', false, true), 66 | _P('module', '', '', 8, config.MODULES, false, true), 67 | _P('password', '', '', 2, 'drive password', false, false), 68 | _P('readme', '', '', 2, 'markdown supported', true, false), 69 | // _P('desc', '', '', 2, 'short desc', false, false), 70 | _P('hidden', [], '當前想要隱藏的文件或文件夾,例如 /images/today, /video/something.mp4', 2, '', false, false), 71 | ]; 72 | config.commonMParams = commonMParams; 73 | 74 | config.getAdminHtml = function (baseURL, version) { 75 | return `Just One Point
`; 80 | }; 81 | 82 | module.exports = config; 83 | -------------------------------------------------------------------------------- /lib/core/app.js: -------------------------------------------------------------------------------- 1 | const { RTError, cookie2Str, query2Obj } = require('../utils/node'); 2 | 3 | class AppRequest { 4 | constructor({ method, path, headers, body, query, cookies, baseURL, ip }) { 5 | // UpperCase 6 | this.method = method; 7 | 8 | // parse path, path is just path,not url 9 | this.path = decodeURIComponent(/^[^?]+/.exec(path)[0]); 10 | 11 | this.headers = headers; 12 | 13 | // parse body, stream is not supposed 14 | this.body = typeof body === 'object' ? body : {}; 15 | if (method === 'POST' && typeof body === 'string' && headers['content-type']) { 16 | if (headers['content-type'].includes('application/x-www-form-urlencoded')) { 17 | this.body = query2Obj(body); 18 | } else if (headers['content-type'].includes('application/json')) { 19 | this.body = JSON.parse(body); 20 | } 21 | } 22 | 23 | // parse query, object or like ?a=1&b=2 or a=1&b=2 or /a/b?aa 24 | this.query = typeof query === 'string' ? query2Obj(query[0] === '/' ? /^[^?]+(.*)$/.exec(query)[1] : query) : query; 25 | 26 | // parse cookie 27 | this.cookies = cookies ? cookies : headers.cookie ? query2Obj(headers.cookie.replace(/;\s/g, '&')) : {}; 28 | 29 | // empty or like https://example.com or https://example.com/sub 30 | if (baseURL) { 31 | this.baseURL = baseURL; 32 | const p0 = new URL(baseURL).pathname; 33 | this.baseURLP0 = p0.endsWith('/') ? p0.slice(0, -1) : p0; 34 | } else { 35 | this.baseURL = 'https://' + headers.host; 36 | this.baseURLP0 = ''; 37 | } 38 | this.ip = ip; 39 | } 40 | } 41 | 42 | class AppResponse { 43 | constructor() { 44 | this.headers = { 'content-type': 'text/html; charset=utf-8' }; 45 | this.body = '[Default Message]'; 46 | this.update(200); 47 | } 48 | 49 | update(status, type = '', data = { message: 'success' }) { 50 | this.status = status; 51 | this.type = type; 52 | this.data = data; 53 | } 54 | 55 | get isPlain() { 56 | return this.type === ''; 57 | } 58 | 59 | get isFile() { 60 | return this.type === 'file'; 61 | } 62 | 63 | get isFolder() { 64 | return this.type === 'folder'; 65 | } 66 | 67 | get isList() { 68 | return this.type === 'list'; 69 | } 70 | 71 | get isRaw() { 72 | return this.type === 'raw'; 73 | } 74 | 75 | addCookie(name, value, options) { 76 | if (!this.headers['set-cookie']) { 77 | this.headers['set-cookie'] = []; 78 | } 79 | this.headers['set-cookie'].push(cookie2Str(name, value, options)); 80 | } 81 | } 82 | 83 | class AppContext { 84 | constructor(request) { 85 | this.request = new AppRequest(request); 86 | this.response = new AppResponse(); 87 | this.state = { level: 0, time: Date.now(), p1: '', p2: '/' }; 88 | } 89 | 90 | get path() { 91 | return this.state.p1 + this.state.p2; 92 | } 93 | 94 | throw(status, msg, properties) { 95 | throw new RTError(status, msg, properties); 96 | } 97 | 98 | assert(value, status, message, properties) { 99 | if (!value) { 100 | this.throw(status, message, properties); 101 | } 102 | } 103 | 104 | respond(status, data = 'success') { 105 | if (typeof data === 'string') { 106 | this.response.update(status, '', { message: data }); 107 | } else { 108 | this.response.update(status, '', data); 109 | } 110 | } 111 | 112 | respondList(list, nextToken = null) { 113 | this.response.update(200, 'list', { list, nextToken }); 114 | } 115 | 116 | respondOne(item, down = null) { 117 | if (item.type === 0) { 118 | this.response.update(200, 'file', { file: item }); 119 | this.response.down = down; 120 | } else { 121 | this.response.update(200, 'folder', { folder: item }); 122 | } 123 | } 124 | 125 | respondRaw(status, headers = {}, body) { 126 | headers['content-type'] = headers['content-type'] || 'text/html; charset=utf-8'; 127 | this.response.update(status, 'raw', '[raw object]'); 128 | this.response.headers = headers; 129 | this.response.body = body; 130 | } 131 | 132 | redirect(url, always = false) { 133 | if (url.startsWith('&')) { 134 | const { path, query, baseURL } = this.request; 135 | url = baseURL + encodeURI(path) + '?' + new URLSearchParams(Object.assign({}, query, query2Obj(url))).toString(); 136 | } else if (url.startsWith('?')) { 137 | const { path, baseURL } = this.request; 138 | url = baseURL + encodeURI(path) + url; 139 | } else if (!url.startsWith('//') && url.startsWith('/')) { 140 | url = this.request.baseURL + encodeURI(url); 141 | } 142 | const headers = this.response.headers; 143 | headers.Location = url; 144 | headers['content-type'] = 'text/html; charset=utf-8'; 145 | headers['referrer-policy'] = 'same-origin'; // ali-drive none referrer 146 | this.respondRaw(always ? 301 : 302, headers, `Redirecting to ${url}.`); 147 | } 148 | 149 | addCookie(name, value, options) { 150 | if (this.request.baseURLP0 && options && options.path) { 151 | options.path = this.request.baseURLP0 + options.path; 152 | } 153 | this.response.addCookie(name, value, options); 154 | } 155 | } 156 | 157 | module.exports = { 158 | middleware: [], 159 | use(fn) { 160 | if (typeof fn === 'function' && !this.middleware.includes(fn)) { 161 | this.middleware.push(fn); 162 | } 163 | return this; 164 | }, 165 | async handleRequest(req) { 166 | const ctx = new AppContext(req); 167 | const middleware = this.middleware; 168 | await (async function useMiddleware(ctx, index) { 169 | if (index < middleware.length) { 170 | return middleware[index](ctx, async () => useMiddleware(ctx, index + 1)); 171 | } 172 | })(ctx, 0); 173 | return ctx; 174 | }, 175 | }; 176 | -------------------------------------------------------------------------------- /lib/core/op.js: -------------------------------------------------------------------------------- 1 | const sysConfig = require('../conf/sys-config'); 2 | const logger = require('../utils/logger'); 3 | const { _sha1, NumberUtil, RTError, beautifyObject } = require('../utils/node'); 4 | const packageInfo = require('../../package.json'); 5 | module.exports = { 6 | modules: {}, 7 | themes: {}, 8 | handleRequest: null, 9 | server: { 10 | version: packageInfo.version, 11 | version2: packageInfo.version2, 12 | }, 13 | initialize(starter) { 14 | this.runtime = { 15 | time_start: Date.now(), 16 | time_read: null, 17 | time_save: null, 18 | error_read: null, 19 | error_write: null, 20 | }; 21 | this.starter = starter; 22 | this.loadConfig({ version: -1 }); 23 | }, 24 | 25 | get params() { 26 | return (this.starter.params || []).concat(sysConfig.commonSParams); 27 | }, 28 | 29 | get privateModuleParams() { 30 | return Object.entries(this.modules).reduce((o, [k, m]) => { 31 | o[k] = typeof m.params === 'function' ? m.params() : m.params || []; 32 | return o; 33 | }, {}); 34 | }, 35 | 36 | get needConfig() { 37 | return this.config.version === 1; 38 | }, 39 | 40 | loadConfig(config) { 41 | config = config || {}; 42 | config.site = Object.assign({}, sysConfig.configTemplate, config.site); 43 | config.drives = config.drives || {}; 44 | config.users = config.users || { admin: { password: 'admin' } }; 45 | config.starter = config.starter || {}; 46 | config.version = config.version || 1; 47 | 48 | const root = { $config: {}, $path: '/', next: {}, $cache: {} }; 49 | 50 | Object.entries(config.drives).forEach(([path, c]) => { 51 | const m = this.modules[c.module]; 52 | if (m) { 53 | let p = root; 54 | for (const i of path.split('/').filter((e) => e)) { 55 | if (!p.next[i]) { 56 | p.next[i] = { $config: {}, $path: p.$path + i + '/', next: {}, $cache: {} }; 57 | } 58 | p = p.next[i]; 59 | } 60 | p.$config = c; 61 | p.$module = m; 62 | } else { 63 | logger.warn('no such module: ' + path + ' ' + c.module); 64 | } 65 | }); 66 | 67 | this.root = root; 68 | this.config = config; 69 | this.salt = config.salt || ''; 70 | }, 71 | 72 | async readConfig() { 73 | logger.debug('read config...'); 74 | this.runtime.time_read = Date.now(); 75 | this.runtime.error_read = null; 76 | return this.starter 77 | .readConfig() 78 | .catch((e) => { 79 | this.runtime.error_read = e; 80 | logger.warn('read config... err:' + this.runtime.time_read); 81 | logger.warn(e); 82 | throw new RTError(500, 'ReadError', { msg: e.message }); 83 | }) 84 | .then((d) => { 85 | logger.debug('read config... ok'); 86 | this.loadConfig(d); 87 | }); 88 | }, 89 | 90 | // @Todo 后续可以考虑使用version解决 nowsh 异步保存的问题 91 | async saveConfig(msg = '') { 92 | logger.debug('save config...'); 93 | this.runtime.time_save = Date.now(); 94 | this.runtime.last_save = msg; 95 | this.runtime.error_write = null; 96 | const copy = beautifyObject(this.config); 97 | copy.versionLast = copy.version; 98 | copy.version = Date.now(); 99 | return await this.starter 100 | .writeConfig(copy, copy.starter) 101 | .then((m) => { 102 | logger.debug('save config... ok'); 103 | this.config.version = copy.version; 104 | this.config.versionLast = copy.versionLast; 105 | this.salt = copy.salt; 106 | return m || 'success'; 107 | }) 108 | .catch((err) => { 109 | this.runtime.error_write = err; 110 | logger.error('save config... err:' + this.runtime.time_save); 111 | throw new RTError(500, 'SaveError', { msg: err.message }); 112 | }); 113 | }, 114 | 115 | sign(text, hours = 24, len = 16) { 116 | const time = NumberUtil.to62(Math.floor(Date.now() / 1000 / 3600 + hours)); 117 | return time + _sha1(this.salt + time + text).slice(0, len - 4); 118 | }, 119 | verify(text, sign, len = 16) { 120 | const time = NumberUtil.parse62(sign.slice(0, 4)) * 3600 * 1000; 121 | return time > Date.now() && sign.slice(4) === _sha1(this.salt + sign.slice(0, 4) + text).slice(0, len - 4); 122 | }, 123 | 124 | signUser(name) { 125 | const { password } = this.config.users[name]; 126 | return name + '.' + this.sign('$' + name + '.' + password + '$', 24); 127 | }, 128 | verifyUser(token) { 129 | const [n, s] = token.split('.'); 130 | const user = this.config.users[n]; 131 | return user && this.verify('$' + n + '.' + user.password + '$', s) && n; 132 | }, 133 | 134 | deepestNode(path) { 135 | let p = this.root; 136 | for (const i of path.split('/').filter((e) => e)) { 137 | if (p.next[i]) { 138 | p = p.next[i]; 139 | } else { 140 | return p; 141 | } 142 | } 143 | return p; 144 | }, 145 | 146 | // 设置p1 p2 $node, 调用模块前必须调用此函数以确认调用哪一个 147 | parsePathBeforeInvoke(ctx, path) { 148 | const node = this.deepestNode(path); 149 | const p1 = node.$path.slice(0, -1); 150 | ctx.assert(path.startsWith(p1), 400, 'InvalidRequestPath', { path, format: p1 + '**' }); 151 | const p2 = path.slice(node.$path.length - 1); 152 | ctx.$node = node; 153 | ctx.state.p1 = p1; 154 | ctx.state.p2 = p2; 155 | }, 156 | }; 157 | -------------------------------------------------------------------------------- /lib/middleware/access-control.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const op = require('../core/op'); 3 | const { AC_PASS_FILE } = require('../conf/sys-config'); 4 | 5 | /** 6 | * @errors [Unauthorized,ItemNotExist] 7 | */ 8 | module.exports = async (ctx, next) => { 9 | const { hidden, password } = ctx.$node.$config; 10 | if (ctx.state.level === 0) { 11 | // hidden file 12 | if (hidden && hidden.length > 0) { 13 | ctx.assert(!hidden.find((e) => ctx.state.p2.startsWith(e)), 404, 'ItemNotExist', { path: ctx.state.p1 + ctx.state.p2 }); 14 | } 15 | // drive-pass 16 | if (password) { 17 | checkPass(ctx, 'drive_pass', password, ctx.state.p1 + '/'); 18 | } 19 | } 20 | 21 | await next(); 22 | 23 | if (ctx.state.level === 0 && ctx.response.isList) { 24 | // hidden list item 25 | if (hidden && hidden.length > 0) { 26 | const p2 = ctx.state.p2; 27 | const h = hidden.map((e) => (e.startsWith(p2) ? e.slice(p2.length) : null)).filter((e) => e && !e.includes('/')); 28 | if (h.length > 0) { 29 | ctx.response.data.list = ctx.response.data.list.filter((e) => !h.includes(e.name)); 30 | } 31 | } 32 | 33 | // list-pass 34 | const o = ctx.response.data.list.reduce( 35 | (o, e) => { 36 | if (e.name.startsWith(AC_PASS_FILE)) { 37 | o[0].push(e.name.slice(AC_PASS_FILE.length)); 38 | } else { 39 | o[1].push(e); 40 | } 41 | return o; 42 | }, 43 | [[], []] 44 | ); 45 | ctx.response.data.list = o[1]; 46 | if (o[0].length > 0) { 47 | checkPass(ctx, 'list_pass', o[0], ctx.state.p1 + ctx.state.p2); 48 | } 49 | } 50 | }; 51 | 52 | function checkPass(ctx, name, pass, path) { 53 | const { cookies, body, method, query } = ctx.request; 54 | 55 | let type = 'empty'; 56 | const uname = name.toUpperCase(); 57 | if (cookies[uname]) { 58 | type = 'expired'; 59 | if (op.verify(uname + path, cookies[uname])) { 60 | logger.log('use cookie:' + cookies[uname]); 61 | return; 62 | } 63 | } 64 | 65 | if (method === 'POST' && (body[name] || body.password)) { 66 | type = 'wrong'; 67 | // 可以使用通用字段password,也可以用对应的专用字段name 68 | const uPass = body[name] || body.password; 69 | if (uPass === pass || (Array.isArray(pass) && pass.includes(uPass))) { 70 | // 单个云盘登录 71 | logger.log('use pass:' + uPass); 72 | ctx.addCookie(uname, op.sign(uname + path, 24 * 31), { path, maxAge: 3600 * 24 * 30 }); 73 | return; 74 | } 75 | } 76 | 77 | // 分享专用 78 | if (method === 'GET' && query[name]) { 79 | type = 'invalid'; 80 | if (op.verify(uname + path, query[name])) { 81 | ctx.addCookie(uname, op.sign(uname + path, 24 * 31), { path, maxAge: 3600 * 24 * 30 }); 82 | return; 83 | } 84 | } 85 | 86 | ctx.throw(401, 'Unauthorized', { field: name, type }); 87 | } 88 | -------------------------------------------------------------------------------- /lib/middleware/admin.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const sysConfig = require('../conf/sys-config'); 3 | const op = require('../core/op'); 4 | const SimpleRouter = require('../utils/simple-router'); 5 | const { P } = require('../utils/node'); 6 | const { beautifyObject } = require('../utils/node'); 7 | const CryptoJS = require('../utils/crypto-js-aes'); 8 | 9 | const { PATH_API, PATH_ADMIN, PATH_DOWN, PATH_SHARE, SHARE_ENCRYPT_AESKEY } = sysConfig; 10 | 11 | const api_router = new SimpleRouter(); 12 | api_router.setDefault((ctx) => { 13 | ctx.throw(400, 'UnsupportedAPI', { path: ctx.request.path }); 14 | }); 15 | 16 | function equalsObj(a, b) { 17 | return JSON.stringify(beautifyObject(a)) === JSON.stringify(beautifyObject(b)); 18 | } 19 | 20 | function paramsCopy(params, src) { 21 | if (!src) { 22 | return {}; 23 | } 24 | const c = {}; 25 | params.forEach(({ name }) => { 26 | if (src[name]) { 27 | c[name] = src[name]; 28 | } 29 | }); 30 | return c; 31 | } 32 | 33 | async function versionCheckAndLazySave(a, f, v, ctx) { 34 | if (!op.needConfig && equalsObj(a[0], a[1])) { 35 | logger.info('Nothing Changed, Lazy Save'); 36 | ctx.respond(200, { message: 'Nothing Changed', version: op.config.version }); 37 | } else { 38 | ctx.assert(v === op.config.version, 400, 'InvalidVersion', { version: op.config.version }); 39 | f(); 40 | ctx.respond(200, { message: await op.saveConfig(), version: op.config.version }); 41 | } 42 | } 43 | 44 | // 读写基本配置 45 | api_router.get('config/basic', async (ctx) => { 46 | const params = op.params; 47 | if (op.needConfig) { 48 | [ 49 | P('admin_salt', String(Math.random()), '签名salt,仅限输入一次,以后不可更改', 9, '', false, false), 50 | P('admin_pass', 'admin', '密码', 9, '', false, true), 51 | P('admin_name', 'admin', '用户名,仅限输入一次,以后不可更改', 9, '', false, true), 52 | ].forEach((e) => params.unshift(e)); 53 | } 54 | 55 | ctx.respond(200, { 56 | basic: Object.assign({}, paramsCopy(sysConfig.commonSParams, op.config.site), paramsCopy(op.starter.params, op.config.starter)), 57 | params: params, 58 | version: op.config.version, 59 | }); 60 | }); 61 | 62 | api_router.post('config/basic', async (ctx) => { 63 | const { basic, version } = ctx.request.body; 64 | const c0 = paramsCopy(sysConfig.commonSParams, basic); 65 | const c1 = paramsCopy(op.starter.params, basic); 66 | const flag = op.needConfig; 67 | await versionCheckAndLazySave( 68 | [ 69 | [op.config.site, op.config.starter], 70 | [c0, c1], 71 | ], 72 | () => { 73 | if (op.config.version === 1) { 74 | op.config.salt = basic.admin_salt; 75 | const users = {}; 76 | users[basic.admin_name] = { password: basic.admin_pass }; 77 | op.config.users = users; 78 | } 79 | op.config.site = c0; 80 | op.config.starter = c1; 81 | }, 82 | version, 83 | ctx 84 | ); 85 | if (flag) { 86 | ctx.response.data.token = op.signUser(basic.admin_name); 87 | } 88 | }); 89 | 90 | // 获取所有的云盘信息,增加乐观锁,弥补token不能识别多处登录的问题 91 | api_router.get('config/drives', async (ctx) => { 92 | ctx.respond(200, { 93 | drives: op.config.drives, 94 | moduleParams: sysConfig.commonMParams, 95 | privateModuleParams: op.privateModuleParams, 96 | version: op.config.version, 97 | }); 98 | }); 99 | 100 | // 如果要删除一个盘,需要传递一个空值,否则不执行删除操作 101 | api_router.post('config/drives', async (ctx) => { 102 | const copy = beautifyObject(op.config.drives); 103 | const { drives, version } = ctx.request.body; 104 | Object.entries(drives).forEach(([p, c]) => { 105 | if (!c) { 106 | delete copy[p]; 107 | } else { 108 | copy[p] = paramsCopy(sysConfig.commonMParams, c); 109 | const m = op.modules[c.module]; 110 | copy[p].config = m ? paramsCopy(typeof m.params === 'function' ? m.params() : m.params, c.config) : {}; 111 | } 112 | }); 113 | await versionCheckAndLazySave( 114 | [op.config.drives, copy], 115 | () => { 116 | op.config.drives = copy; 117 | op.loadConfig(op.config); 118 | }, 119 | version, 120 | ctx 121 | ); 122 | }); 123 | 124 | api_router.post('config/export', async (ctx) => { 125 | const u = op.config.users[ctx.state.user]; 126 | ctx.assert(ctx.request.body.password === u.password, 400, 'InvalidUserAuth', { user: ctx.state.user }); 127 | ctx.respond(200, { config: op.config }); 128 | }); 129 | 130 | // 整体导入的配置 不再检验version的有效性 131 | api_router.post('config/import', async (ctx) => { 132 | const u = op.config.users[ctx.state.user]; 133 | const { config, password } = ctx.request.body; 134 | ctx.assert(password === u.password, 400, 'InvalidUserAuth', { user: ctx.state.user }); 135 | await versionCheckAndLazySave( 136 | [op.config, config], 137 | () => { 138 | op.loadConfig(config); 139 | }, 140 | op.config.version, 141 | ctx 142 | ); 143 | }); 144 | 145 | // 修改密码 146 | api_router.post('user/password', async (ctx) => { 147 | const u = op.config.users[ctx.state.user]; 148 | const { password0, password, version } = ctx.request.body; 149 | ctx.assert(password0 === u.password, 400, 'InvalidUserAuth', { user: ctx.state.user }); 150 | await versionCheckAndLazySave( 151 | [u.password, password], 152 | () => { 153 | // change password 154 | u.password = password; 155 | }, 156 | version, 157 | ctx 158 | ); 159 | }); 160 | 161 | api_router.get('system/runtime', async (ctx) => { 162 | ctx.respond(200, { runtime: op.runtime, version: op.config.version }); 163 | }); 164 | 165 | api_router.get('system/reload', async (ctx) => { 166 | op.config.version = -1; 167 | ctx.respond(200); 168 | }); 169 | 170 | api_router.prefix('file:', async (ctx, next, path) => { 171 | const m = ctx.request.method; 172 | const b = ctx.request.body; 173 | const $data = (ctx.$data = {}); 174 | b.path && (path = b.path); 175 | ctx.assert(path, 400, 'InvalidRequestParam', { expect: ['path'] }); 176 | if (m === 'GET') { 177 | $data.command = 'ls'; 178 | } else if (m === 'DELETE') { 179 | $data.command = 'rm'; 180 | } else if (m === 'POST') { 181 | const c = ($data.command = b.command); 182 | if (c === 'mkdir' || c === 'ren' || c === 'touch' || c === 'upload') { 183 | ctx.assert(($data.name = b.name), 400, 'InvalidRequestParam', { expect: ['name'] }); 184 | if (c === 'touch') { 185 | const { content, base64 } = b; 186 | ctx.assert(content !== undefined || base64, 400, 'InvalidRequestParam', { expect: ['content||base64'] }); 187 | $data.mime = b.mime || 'text/plain'; 188 | if (content) { 189 | $data.content = Buffer.from(content, 'utf-8'); 190 | } else if (base64) { 191 | // base64 192 | $data.content = Buffer.from(base64, 'base64'); 193 | } 194 | } 195 | } else if (c === 'mv' || c === 'cp') { 196 | const { desPath } = b; 197 | ctx.assert(path !== desPath && op.deepestNode(path) === op.deepestNode(desPath), 400, 'InvalidRequestParam', { expect: ['desPath'] }); 198 | $data.desPath = desPath.slice(op.deepestNode(desPath).$path.length - 1); 199 | } else { 200 | ctx.throw(400, 'InvalidRequestParam', { expect: ['command'] }); 201 | } 202 | } 203 | op.parsePathBeforeInvoke(ctx, path); 204 | await next(); 205 | }); 206 | 207 | const router = new SimpleRouter(); 208 | 209 | router.prefix(PATH_ADMIN, async (ctx, _, path) => { 210 | if (path === '') { 211 | ctx.respondRaw(200, {}, sysConfig.getAdminHtml(ctx.request.baseURL, op.config.version)); 212 | } else { 213 | ctx.assert(ctx.state.level > 0, 401, 'Unauthorized', { 214 | field: 'admin-token', 215 | type: 'login please', 216 | }); 217 | } 218 | }); 219 | router.prefix(PATH_API, async (ctx, next, path) => { 220 | if (path === 'login') { 221 | const { username, password } = ctx.request.body; 222 | const u = op.config.users[username]; 223 | ctx.assert(u && password && u.password === password, 400, 'InvalidUserAuth', { username }); 224 | const token = op.signUser(username); 225 | ctx.addCookie('X_TOKEN', token, { path: '/' }); 226 | ctx.respond(200, { token, version: op.config.version }); 227 | } else if (path === 'public/site') { 228 | ctx.respond(200, { 229 | site: op.config.site, 230 | drives: Object.entries(op.config.drives).map(([path, { readme = '' }]) => ({ path, readme })), 231 | version: op.server.version, 232 | version2: op.server.version2, 233 | }); 234 | } else { 235 | ctx.assert(op.needConfig || ctx.state.level > 0, 401, 'UnauthorizedToken', { token: null }); 236 | await api_router.handle(ctx, next, path); 237 | } 238 | }); 239 | router.prefix(PATH_DOWN, async (ctx, next, path) => { 240 | ctx.$data = { command: 'download' }; 241 | op.parsePathBeforeInvoke(ctx, path); 242 | await next(); 243 | }); 244 | router.prefix(PATH_SHARE, async (ctx, next, path) => { 245 | decodeEncPart = (encBase64, key) => { 246 | // console.log(encBase64); 247 | const encRaw = CryptoJS.enc.Base64.parse(encBase64.replace(/_/g, '/').replace(/-/g, '+')); 248 | // console.log(encRaw.toString()); 249 | const iv = CryptoJS.enc.Hex.parse(encRaw.toString().slice(0, 32)); // first 16 bytes 250 | // console.log(iv.toString()); 251 | const encData = CryptoJS.enc.Hex.parse(encRaw.toString().slice(32)); // remaining 252 | // console.log(encData.toString()); 253 | //const iv = CryptoJS.enc.Hex.parse('00000000000000000000000000000000'); 254 | const rawkey = CryptoJS.enc.Utf8.parse(key); 255 | // console.log(encData.toString()); 256 | const aesDecryptor = CryptoJS.algo.AES.createDecryptor(rawkey, { iv: iv }); // aes-128-cbc 257 | let decData = aesDecryptor.finalize(encData); 258 | let decString = CryptoJS.enc.Utf8.stringify(decData); 259 | return decString; 260 | } 261 | let path_parts = path.split('/'); 262 | encryptedBase64 = path_parts.pop(); 263 | param = decodeEncPart(encryptedBase64, op.config.site.share_aeskey); 264 | console.log("Decoded encrypted part: " + param); 265 | param = JSON.parse(param); 266 | if (param.gen === undefined || param.exp === undefined || param.path === undefined) { 267 | ctx.throw(400, 'Invalid Encrypted Param'); 268 | } 269 | if (Date.now() / 1000 > param.gen + param.exp) { 270 | ctx.throw(401, 'Share Link Expired'); 271 | } 272 | 273 | path_parts.push(param.path); 274 | new_path = '/' + path_parts.join('/'); 275 | console.log("newpath: " + new_path); 276 | ctx.state.level = 1; 277 | 278 | ctx.state.html = true; 279 | ctx.$data = { command: 'ls' }; 280 | op.parsePathBeforeInvoke(ctx, new_path); 281 | await next(); 282 | }); 283 | router.setDefault(async (ctx, next) => { 284 | ctx.state.html = ctx.request.headers.accept !== 'application/json'; 285 | ctx.state.useCache = ctx.request.query.refresh === undefined; 286 | ctx.$data = { command: 'ls' }; 287 | op.parsePathBeforeInvoke(ctx, ctx.request.path); 288 | await next(); 289 | }); 290 | 291 | module.exports = async (ctx, next) => { 292 | const { cookies, path, headers } = ctx.request; 293 | const token = headers['x-token'] || cookies.X_TOKEN; 294 | if (token) { 295 | const user = op.verifyUser(token); 296 | if (user) { 297 | ctx.state.user = user; 298 | ctx.state.level = 1; 299 | } else { 300 | ctx.addCookie('X_TOKEN', '', { path: '/' }); 301 | ctx.throw(401, 'UnauthorizedToken', { token }); 302 | } 303 | } 304 | await router.handle(ctx, next, path); 305 | }; 306 | -------------------------------------------------------------------------------- /lib/middleware/cache-control.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const { RTError } = require('../utils/node'); 3 | const { CACHE_TIME_FILE, CACHE_TIME_LIST } = require('../conf/sys-config'); 4 | module.exports = async (ctx, next) => { 5 | const path = ctx.state.p1 + ctx.state.p2; 6 | // use cache 7 | if (ctx.state.useCache) { 8 | const m = oneCache.getMsg(path, ctx.$data.page); 9 | if (m) { 10 | logger.info('cache hit'); 11 | if (m.list) { 12 | ctx.respondList(m.list, m.nextToken); 13 | } else if (m.one) { 14 | ctx.respondOne(m.one, m.down); 15 | } 16 | ctx.response.data.cached = m.time; 17 | return; 18 | } 19 | } 20 | 21 | await next(); 22 | 23 | if (ctx.response.isList) { 24 | const d = ctx.response.data; 25 | oneCache.addList(path, d.list, d.nextToken, ctx.$data.page); 26 | } else if (ctx.response.isFile) { 27 | oneCache.addOne(path, ctx.response.data.file, ctx.response.down); 28 | } else if (ctx.response.isFolder) { 29 | oneCache.addOne(path, ctx.response.data.folder); 30 | } 31 | }; 32 | 33 | class OneCache { 34 | constructor() { 35 | this.root = { next: {} }; 36 | } 37 | 38 | addList(path, list, nextToken, page = 0) { 39 | const p = this.getNode(path, true); 40 | const oNext = p.next; 41 | const next = {}; 42 | const time = Date.now(); 43 | list.forEach((e) => { 44 | next[e.name] = { 45 | value: e, 46 | next: oNext[e.name] ? oNext[e.name].next : {}, 47 | time: e.url || e.type !== 0 ? time : 0, 48 | }; 49 | }); 50 | p.next = next; 51 | 52 | if (page || nextToken) { 53 | // 一锅炖不下 54 | if (!p.pages) { 55 | p.pages = {}; 56 | } 57 | const t = { list, nextToken, listTime: Date.now() }; 58 | t.time = t.listTime; 59 | p.pages[page] = t; 60 | } else { 61 | p.listTime = Date.now(); 62 | } 63 | } 64 | 65 | addOne(path, item, down = null) { 66 | const p = this.getNode(path, true); 67 | p.value = item; 68 | p.down = down ? JSON.stringify(down) : null; 69 | p.time = Date.now(); 70 | } 71 | 72 | getNode(path, addIfAbsent) { 73 | let p = this.root; 74 | for (const i of path.split('/').filter((e) => e)) { 75 | if (!p.next[i]) { 76 | if (!addIfAbsent) { 77 | return; 78 | } 79 | p.next[i] = { next: {} }; 80 | } 81 | p = p.next[i]; 82 | } 83 | return p; 84 | } 85 | 86 | getMsg(path, page = 0) { 87 | const p = this.getNode(path); 88 | if (!p) { 89 | return; 90 | } 91 | 92 | if (path.endsWith('/')) { 93 | if ((p.listTime || 0) > Date.now() - CACHE_TIME_LIST) { 94 | return { list: Object.values(p.next).map((e) => e.value), time: p.listTime }; 95 | } 96 | if (p.pages && p.pages[page] && p.pages[page].listTime > Date.now() - CACHE_TIME_LIST) { 97 | return p.pages[page]; 98 | } 99 | if (p.value && p.value.type === 0) { 100 | throw new RTError(400, 'ItemIsFile'); 101 | } 102 | } else { 103 | if ((p.time || 0) > Date.now() - CACHE_TIME_FILE) { 104 | return { one: p.value, down: p.down ? JSON.parse(p.down) : null, time: p.time }; 105 | } 106 | } 107 | } 108 | 109 | drop(path = '/') { 110 | if (path.endsWith('/')) { 111 | path = path.slice(0, -1); 112 | } 113 | const pPath = path.slice(0, path.lastIndexOf('/')); 114 | if (pPath === '') { 115 | this.root = { next: {} }; 116 | return; 117 | } 118 | const pp = this.getNode(pPath); 119 | if (!pp) { 120 | return; 121 | } 122 | const name = path.slice(path.lastIndexOf('/') + 1); 123 | delete pp.next[name]; 124 | delete pp.pages; 125 | } 126 | } 127 | 128 | const oneCache = new OneCache(); 129 | -------------------------------------------------------------------------------- /lib/middleware/header.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const op = require('../core/op'); 3 | 4 | module.exports = async (ctx, next) => { 5 | const { method, path, query, headers, ip } = ctx.request; 6 | logger.log(method + ' ' + path + ' ' + new URLSearchParams(query).toString() + ' ' + ip); 7 | // OPTIONS method for CORS 8 | if (method === 'OPTIONS') { 9 | // @Todo origin检查 10 | ctx.respondRaw( 11 | 204, 12 | { 13 | 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', 14 | 'access-control-allow-headers': 'content-type,content-range,x-token', 15 | 'access-control-max-age': '86400', 16 | 'access-control-allow-origin': headers.origin || '*', 17 | }, 18 | '' 19 | ); 20 | return; 21 | } 22 | 23 | if (path === '/favicon.ico') { 24 | ctx.redirect(op.config.site.logo); 25 | ctx.response.status = 301; 26 | return; 27 | } 28 | 29 | await next(); 30 | 31 | // allow * temporarily 32 | if (checkCors(headers.origin, op.config.site.cors)) { 33 | ctx.response.headers['access-control-allow-origin'] = '*'; 34 | } 35 | }; 36 | 37 | function checkCors(origin, cors) { 38 | if (!origin) { 39 | return true; 40 | } 41 | if (cors) { 42 | return cors.includes('*') || cors.includes(origin); 43 | } else { 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/middleware/invoke-module.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const op = require('../core/op'); 3 | const { ID_SIGN_LEN } = require('../conf/sys-config'); 4 | const { mime } = require('../utils/node'); 5 | 6 | function signId(p2, id) { 7 | // 1-10年有效期, 纯粹是为了让生成的签名不那么一样 8 | return id + op.sign(p2 + id, Math.floor(Math.random() * 78840) + 8760, ID_SIGN_LEN); 9 | } 10 | 11 | function verifyId(p2, id, ctx) { 12 | if (id) { 13 | // verify id 14 | const oid = id.slice(0, id.length - ID_SIGN_LEN); 15 | if (oid && op.verify((p2.endsWith('/') ? p2.slice(0, -1) : p2) + oid, id.slice(id.length - ID_SIGN_LEN), ID_SIGN_LEN)) { 16 | ctx.$data.id = oid; 17 | } else { 18 | ctx.throw(403, 'InvalidId', { id }); 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * @errors [CommandNotAllowed,ItemNotExist,InvalidId] 25 | */ 26 | module.exports = async (ctx) => { 27 | const node = ctx.$node; 28 | const cmd = ctx.$data.command; 29 | const p2 = (ctx.$data.path = ctx.state.p2); 30 | if (p2 === '') { 31 | ctx.assert(cmd === 'ls', 403, 'CommandNotAllowed', { command: cmd }); 32 | const p1 = ctx.state.p1; 33 | ctx.respondOne({ 34 | type: 3, 35 | name: p1.slice(p1.lastIndexOf('/') + 1) || '$root', 36 | size: null, 37 | mime: '', 38 | time: new Date().toISOString(), 39 | }); 40 | } else if (node.$module) { 41 | verifyId(p2, ctx.request.query.id, ctx); 42 | logger.log('use: ' + node.$config.module + ', drivePath:' + ctx.state.p1); 43 | ctx.assert(node.$module[cmd], 403, 'CommandNotAllowed', { command: cmd }); 44 | await node.$module.handle(node.$config.config, ctx.$data, node.$cache, ctx); 45 | } else { 46 | ctx.assert(cmd === 'ls', 403, 'CommandNotAllowed', { command: cmd }); 47 | ctx.assert(ctx.state.p2 === '/', 404, 'ItemNotExist', { path: ctx.state.p1 + ctx.state.p2 }); 48 | ctx.respondList([]); 49 | } 50 | 51 | if (ctx.response.isList) { 52 | const list = ctx.response.data.list; 53 | list.forEach((e) => { 54 | if (e.id) { 55 | e.id = signId(p2 + e.name, e.id); 56 | } 57 | if (e.mime === undefined) { 58 | e.mime = e.type === 0 ? mime.get(e.name) : ''; 59 | } 60 | }); 61 | if (ctx.state.p2 === '/') { 62 | Object.keys(node.next).forEach((e) => 63 | list.push({ 64 | type: 3, 65 | name: e, 66 | size: null, 67 | mime: '', 68 | time: new Date().toISOString(), 69 | }) 70 | ); 71 | } 72 | // sort 73 | list.sort((e1, e2) => e2.type - e1.type || e1.name.localeCompare(e2.name)); 74 | } else if (ctx.response.isFile) { 75 | const e = ctx.response.data.file; 76 | if (e.id) { 77 | // 文档有要求,返回file时,必须为规范路径 78 | e.id = signId(p2, e.id); 79 | } 80 | // 简化模块代码重复度 81 | if (e.mime === undefined) { 82 | e.mime = mime.get(e.name); 83 | } 84 | } else if (ctx.response.isFolder) { 85 | const e = ctx.response.data.folder; 86 | if (e.id) { 87 | // 文档有要求,返回file时,必须为规范路径 88 | e.id = signId(p2, e.id); 89 | } 90 | // 简化模块代码重复度 91 | if (e.mime === undefined) { 92 | e.mime = ''; 93 | } 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /lib/middleware/page-control.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const op = require('../core/op'); 3 | const { PAGE_SIGN_LEN } = require('../conf/sys-config'); 4 | 5 | function signPage(p2, page) { 6 | return page + op.sign(p2 + page, Math.floor(Math.random() * 78840) + 8760, PAGE_SIGN_LEN); 7 | } 8 | 9 | function verifyPage(p2, page, ctx) { 10 | const oPage = page.slice(0, page.length - PAGE_SIGN_LEN); 11 | if (oPage && op.verify(p2 + oPage, page.slice(page.length - PAGE_SIGN_LEN), PAGE_SIGN_LEN)) { 12 | ctx.$data.page = oPage; 13 | logger.log('page: ' + oPage); 14 | } else { 15 | ctx.throw(403, 'InvalidPage', { page }); 16 | } 17 | } 18 | 19 | /** 20 | * @errors [InvalidPage] 21 | */ 22 | module.exports = async (ctx, next) => { 23 | const page = ctx.request.query.page; 24 | if (ctx.state.p2.endsWith('/') && page) { 25 | // verify page 26 | verifyPage(ctx.state.p2, page, ctx); 27 | } 28 | await next(); 29 | if (ctx.response.isList) { 30 | const data = ctx.response.data; 31 | if (data.nextToken) { 32 | data.nextToken = signPage(ctx.state.p2, data.nextToken); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/middleware/template.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const op = require('../core/op'); 3 | const V = require('../utils/view-helper'); 4 | const { RTError } = require('../utils/node'); 5 | 6 | module.exports = async (ctx, next) => { 7 | await next().catch((err) => { 8 | if (err instanceof RTError) { 9 | logger.log('request error:' + err.type); 10 | ctx.respond(err.status, { error: err.type, data: err.data, message: err.message }); 11 | } else { 12 | logger.error(err.stack); 13 | ctx.respond(400, { error: 'UnknownError', data: {}, message: err.message }); 14 | } 15 | }); 16 | 17 | if (ctx.response.isRaw) { 18 | return; 19 | } 20 | 21 | // 解决缓存引起的 多baseURL下 url不变的问题 22 | if (ctx.response.isFile && !ctx.response.data.file.url) { 23 | ctx.response.data.file = Object.assign({}, ctx.response.data.file, { url: ctx.request.baseURL + encodeURI(ctx.state.p1 + ctx.state.p2) }); 24 | } 25 | 26 | if (ctx.state.html && ctx.request.query.json === undefined) { 27 | if (ctx.response.isFile) { 28 | if (ctx.request.query.preview === undefined) { 29 | if (ctx.response.down) { 30 | ctx.response.callback_down = ctx.response.down; 31 | } else { 32 | ctx.redirect(ctx.response.data.file.url); 33 | } 34 | return; 35 | } 36 | } 37 | 38 | const theme = op.themes[op.config.site.theme] || op.themes['w.w.art']; 39 | ctx.$V = new V(ctx); 40 | ctx.response.body = theme.render(ctx); 41 | } else { 42 | ctx.response.headers['content-type'] = 'application/json'; 43 | ctx.response.body = JSON.stringify(ctx.response.data); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /lib/routers/alidrive.js: -------------------------------------------------------------------------------- 1 | const { request, IDHelper, _P } = require('../utils/node'); 2 | const logger = require('../utils/logger'); 3 | const { RTError } = require('../utils/node'); 4 | 5 | // 支持分页、id寻址 6 | 7 | const { PAGE_SIZE } = require('../conf/sys-config'); 8 | 9 | function filter(e) { 10 | const r = { 11 | type: 1, 12 | name: e.name, 13 | time: e.updated_at, 14 | id: e.file_id, 15 | }; 16 | 17 | if (e.type === 'file') { 18 | r.type = 0; 19 | r.mime = e.mimeType; 20 | r.size = e.size; 21 | r.url = e.url; 22 | } 23 | return r; 24 | } 25 | 26 | class AliDrive extends IDHelper { 27 | static async build(config) { 28 | const m = new AliDrive(config); 29 | const { access_token, default_drive_id } = await m.refreshToken(config); 30 | // @warning refresh_token 有效期未知,暂时按永久有效处理 31 | m.access_token = access_token; 32 | m.service.defaults.headers.Authorization = 'Bearer ' + access_token; 33 | m.drive_id = default_drive_id; 34 | return m; 35 | } 36 | 37 | constructor({ root }) { 38 | super(root || 'root'); 39 | this.service = request.create({ 40 | baseURL: 'https://api.aliyundrive.com/v2/', 41 | headers: { 42 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 43 | Origin: 'https://aliyundrive.com', 44 | Accept: '*/*', 45 | 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 46 | Connection: 'keep-alive', 47 | }, 48 | onResponse: (res) => { 49 | const { status, data } = res; 50 | if (status >= 300) { 51 | if (status === 404) { 52 | return Promise.reject(new RTError(404, 'ItemNotExist')); 53 | } 54 | 55 | if (!data || !data.message) { 56 | return Promise.reject(new RTError(500, 'HttpError')); 57 | } 58 | return Promise.reject(new RTError(400, 'ModuleError', data.message)); 59 | } 60 | return data; 61 | }, 62 | }); 63 | } 64 | 65 | async refreshToken({ refresh_token }) { 66 | const data = await this.service.post('https://auth.aliyundrive.com/v2/account/token', { 67 | grant_type: 'refresh_token', 68 | refresh_token, 69 | }); 70 | logger.log('ali drive access_token:' + data.access_token); 71 | return data; 72 | } 73 | 74 | async findChildItem(pid, name) { 75 | return this.service 76 | .post('file/search', { 77 | drive_id: this.drive_id, 78 | limit: 10, 79 | query: `parent_file_id = "${pid}" and name = "${name}"`, 80 | order_by: 'name ASC', 81 | }) 82 | .then((data) => data.items.map(filter).find((e) => e.name === name)); 83 | } 84 | 85 | async itemInfo(file_id) { 86 | return this.service.post('file/get', { drive_id: this.drive_id, file_id }).then(filter); 87 | } 88 | 89 | async fetchList(pid, pageToken = null) { 90 | return this.service 91 | .post('file/list', { 92 | drive_id: this.drive_id, 93 | parent_file_id: pid, 94 | limit: PAGE_SIZE, 95 | all: false, 96 | fields: '*', 97 | order_by: 'name', 98 | order_direction: 'ASC', 99 | marker: pageToken, 100 | }) 101 | .then((data) => ({ 102 | list: data.items.map(filter), 103 | next: data.next_marker, 104 | })); 105 | } 106 | } 107 | 108 | module.exports = { 109 | get params() { 110 | return [_P('refresh_token', '', '', 7, '', false, true), _P('root', '', '', 5, 'root', false, false)]; 111 | }, 112 | async handle(config, data, cache, ctx) { 113 | let $m = cache.$m || {}; 114 | ctx.assert($m.isValid || (config.refresh_token && (cache.$m = $m = await AliDrive.build(config))), 400, 'ConfigError', { fields: ['refresh_token'] }); 115 | data.id = data.id || (await $m.getIDByPath(data.path)); 116 | return this[data.command](data, cache, ctx); 117 | }, 118 | async ls({ path, id, page }, { $m }, ctx) { 119 | if (path.endsWith('/')) { 120 | const { list, next } = await $m.fetchList(id, page); 121 | ctx.assert(list.length > 0 || (await $m.itemInfo(id)).type === 1, 400, 'ItemIsFile'); 122 | ctx.respondList(list, next); 123 | } else { 124 | ctx.respondOne(await $m.itemInfo(id)); 125 | } 126 | }, 127 | }; 128 | -------------------------------------------------------------------------------- /lib/routers/coding.js: -------------------------------------------------------------------------------- 1 | const { request, RTError, IDHelper, _P } = require('../utils/node'); 2 | 3 | const { PAGE_SIZE } = require('../conf/sys-config'); 4 | const ID_DELIMITER = 'l'; 5 | 6 | function filter(e) { 7 | const res = { 8 | type: 1, 9 | name: e.name, 10 | time: new Date(e.updatedAt || e.updated_at).toISOString(), 11 | size: null, 12 | }; 13 | if (!e.folderId) { 14 | res.type = 0; 15 | res.size = e.size; 16 | if (e.url.startsWith('http')) { 17 | res.url = e.url; 18 | } 19 | // 列表没有直链了 20 | } 21 | // 记录下父id id 和类型 22 | res.id = (e.id || e.file_id) + ID_DELIMITER + e.parentId + ID_DELIMITER + res.type; 23 | return res; 24 | } 25 | 26 | function parseId(id) { 27 | const idInfo = id.split(ID_DELIMITER); 28 | return { 29 | id: idInfo[0], 30 | pid: idInfo[1], 31 | type: Number(idInfo[2]), 32 | }; 33 | } 34 | 35 | // 因token能使用的api有限 所以改用cookie eid 默认是一年的有效期 36 | class Coding extends IDHelper { 37 | static async build(config) { 38 | const c = new Coding(config); 39 | // 设置 X-XSRF-TOKEN 40 | await c.fetchList(0, 1); 41 | return c; 42 | } 43 | 44 | constructor({ root, api_url, cookie_eid }) { 45 | super(root); 46 | // service.defaults.headers.common['Authorization'] = 'token ' + api_token; 47 | this.service = request.create({ 48 | baseURL: api_url, 49 | headers: { 50 | Cookie: 'eid=' + cookie_eid, 51 | }, 52 | onResponse: (res) => { 53 | const { status, headers, data } = res; 54 | 55 | if (status >= 300) { 56 | return Promise.reject(new RTError(500, 'HttpError')); 57 | } 58 | 59 | if (data.code !== 0) { 60 | const code = data.code; 61 | if (code === 1302 || code === 1217) { 62 | return Promise.reject(new RTError(400, 'ItemAlreadyExist')); 63 | } 64 | if (code === 1304) { 65 | return Promise.reject(new RTError(404, 'ItemNotExist')); 66 | } 67 | return Promise.reject(new RTError(400, 'ModuleError', 'error coding code:' + data.code)); 68 | } 69 | 70 | if (headers['set-cookie']) { 71 | const match = /XSRF-TOKEN=(?[^;]+);/.exec(headers['set-cookie']); 72 | if (match && match.groups.token) { 73 | this.service.defaults.headers.Cookie = `eid=${cookie_eid};XSRF-TOKEN=${match.groups.token};`; 74 | this.service.defaults.headers['X-XSRF-TOKEN'] = match.groups.token; 75 | } 76 | } 77 | 78 | return data.data; 79 | }, 80 | }); 81 | } 82 | 83 | async findChildItem(pid, name) { 84 | const data = await this.service.get(`folders/${pid}/all/masonry?sortName=name&sortValue=asc&pageSize=10&keyword=${encodeURIComponent(name)}&recursive=false`); 85 | const e = data.list.find((e) => e.name === name); 86 | if (!e) { 87 | throw new RTError(404, 'ItemNotExist'); 88 | } 89 | return { type: e.url ? 0 : 1, name: e.name, id: e.id, pid }; 90 | } 91 | 92 | async fetchList(id, page = 1) { 93 | const data = await this.service.get(`folders/${id}/all/masonry?sortName=name&sortValue=asc&page=${page}&pageSize=${PAGE_SIZE}`); 94 | const list = data.list.map(filter); 95 | const next = data.page < data.totalPage ? data.page + 1 : null; 96 | return { list, next }; 97 | } 98 | 99 | // 1302 100 | async mkdir(pid, name) { 101 | const params = new URLSearchParams(); 102 | params.set('parentId', pid); 103 | params.set('name', name); 104 | return filter(await this.service.post('mkdir', params)); 105 | } 106 | 107 | // 1302 108 | async renameFolder(id, name) { 109 | const params = new URLSearchParams(); 110 | params.set('name', name); 111 | return this.service.put(`folder/${id}`, params); 112 | } 113 | 114 | // 1302 115 | async renameFile(id, name) { 116 | const params = new URLSearchParams(); 117 | params.set('name', name); 118 | return this.service.put(`files/${id}/rename`, params); 119 | } 120 | 121 | // 1217 122 | async touch(pid, name, content) { 123 | const params = new URLSearchParams(); 124 | params.set('name', name); 125 | params.set('content', content); 126 | return this.service.post(`files/${pid}/create`, params); 127 | } 128 | 129 | async delete(ids) { 130 | const params = new URLSearchParams(); 131 | ids.forEach((id) => params.append('fileIds', id)); 132 | return this.service.post(`/files/recycle-bin/async`, params); 133 | } 134 | 135 | // 1304 136 | async fileInfo(id) { 137 | return filter(await this.service.get(`files/${id}/attribute`)); 138 | } 139 | 140 | // 1304 141 | async folderInfo(id) { 142 | return filter(await this.service.get(`folders/${id}/attribute`)); 143 | } 144 | 145 | async move(pid, ids) { 146 | const params = new URLSearchParams(); 147 | params.set('toFolderId', pid); 148 | ids.forEach((id) => params.append('fileIds', id)); 149 | return this.service.post(`files/move/async`, params); 150 | } 151 | 152 | async copy(pid, ids) { 153 | const params = new URLSearchParams(); 154 | params.set('toFolderId', pid); 155 | ids.forEach((id) => params.append('fileIds', id)); 156 | return this.service.post(`files/copy/async`, params); 157 | } 158 | 159 | async asyncTask(jid) { 160 | return this.service.get(`files/async-jobs/${jid}`); 161 | } 162 | 163 | async mulDownload(ids) { 164 | const params = new URLSearchParams(); 165 | ids.forEach((id) => params.append('fileIds', id)); 166 | params.set('withDirName', 'true'); 167 | return this.service.get(`files/mixed/download/`, { params }); 168 | } 169 | } 170 | 171 | module.exports = { 172 | params() { 173 | return [ 174 | _P('api_url', '', '形如: https://<团队名>.coding.net/api/user/<用户名>/project/<项目名>/folder/', 7, '', false, true), 175 | _P('cookie_eid', '', 'cookie中的eid项', 7, '', false, true), 176 | _P('root', '', '根目录或文件夹id', 5, '0', false, false), 177 | ]; 178 | }, 179 | async handle(config, data, cache, ctx) { 180 | if ((cache.etime || 0) < Date.now()) { 181 | const fields = []; 182 | if (!/https:\/\/.+.coding.net\/api\/project\/.+\//.exec(config.api_url)) { 183 | fields.push('api_url'); 184 | } 185 | if (!config.cookie_eid) { 186 | fields.push('cookie_eid'); 187 | } 188 | if (isNaN(Number(config.root))) { 189 | delete config.root; 190 | } 191 | config.root = Number(config.root) || 0; 192 | if (fields.length > 0) { 193 | throw new RTError(400, 'ConfigError', { fields }); 194 | } 195 | 196 | cache.$U = await Coding.build(config); 197 | cache.etime = Date.now() + 3600 * 1000; 198 | } 199 | data._item = data.id ? parseId(data.id) : await cache.$U.getItemByPath(data.path); 200 | 201 | if (data.desPath) { 202 | data._desItem = data.desId ? parseId(data.desId) : await cache.$U.getItemByPath(data.desPath); 203 | } 204 | 205 | return this[data.command](data, cache, ctx); 206 | }, 207 | async ls({ path, page, _item }, { $U }, ctx) { 208 | if (path.endsWith('/')) { 209 | if (_item.type === 0) { 210 | throw new RTError(403, 'ItemIsFile'); 211 | } 212 | const { list, next } = await $U.fetchList(_item.id, page); 213 | ctx.respondList(list, next); 214 | } else { 215 | ctx.respondOne(await $U[_item.type === 0 ? 'fileInfo' : 'folderInfo'](_item.id)); 216 | } 217 | }, 218 | async mkdir({ name, _item }, { $U }) { 219 | if (_item.type === 0) { 220 | throw new RTError(403, 'ItemIsFile'); 221 | } 222 | await $U.mkdir(_item.id, name); 223 | }, 224 | async mv({ _item, _desItem }, { $U }, ctx) { 225 | if (_desItem.type === 0) { 226 | throw new RTError(403, 'ItemIsFile'); 227 | } 228 | const { job_id } = await $U.move(_desItem.id, [_item.id]); 229 | ctx.respond(202, { async: job_id }); 230 | }, 231 | async cp({ _item, _desItem }, { $U }, ctx) { 232 | if (_desItem.type === 0) { 233 | throw new RTError(403, 'ItemIsFile'); 234 | } 235 | const { job_id } = await $U.copy(_desItem.id, [_item.id]); 236 | ctx.respond(202, { async: job_id }); 237 | }, 238 | async rm({ _item }, { $U }, ctx) { 239 | const { job_id } = await $U.delete([_item.id]); 240 | ctx.respond(202, { async: job_id }); 241 | }, 242 | async ren({ name, _item }, { $U }) { 243 | await $U[_item.type === 0 ? 'renameFile' : 'renameFolder'](_item.id, name); 244 | }, 245 | async touch({ name, content, _item }, { $U }, ctx) { 246 | await $U.touch(_item.id, name, content); 247 | ctx.respond(201); 248 | }, 249 | }; 250 | -------------------------------------------------------------------------------- /lib/routers/googledrive.js: -------------------------------------------------------------------------------- 1 | const { request, RTError, IDHelper, _P } = require('../utils/node'); 2 | const logger = require('../utils/logger'); 3 | 4 | // 支持分页、id寻址、中转下载 5 | 6 | const { PAGE_SIZE } = require('../conf/sys-config'); 7 | 8 | function filter(e) { 9 | // 处理shortcut 10 | if (e.shortcutDetails) { 11 | e.id = e.shortcutDetails.targetId; 12 | e.mimeType = e.shortcutDetails.targetMimeType; 13 | } 14 | 15 | const r = { 16 | type: 1, 17 | name: e.name, 18 | time: e.modifiedTime, 19 | id: e.id, 20 | }; 21 | 22 | if (e.mimeType !== 'application/vnd.google-apps.folder') { 23 | r.type = 0; 24 | r.mime = e.mimeType; 25 | r.size = Number(e.size); 26 | } 27 | return r; 28 | } 29 | 30 | class GoogleDrive extends IDHelper { 31 | static async build(config) { 32 | const g = new GoogleDrive(config); 33 | const { access_token } = await g.refreshToken(config); 34 | g.access_token = access_token; 35 | g.service.defaults.headers.Authorization = 'Bearer ' + access_token; 36 | return g; 37 | } 38 | 39 | constructor({ root }) { 40 | super(root || 'root'); 41 | this.service = request.create({ 42 | baseURL: 'https://www.googleapis.com/', 43 | onResponse: (res) => { 44 | const { status, data } = res; 45 | if (status >= 300) { 46 | if (status === 404) { 47 | return Promise.reject(new RTError(404, 'ItemNotExist')); 48 | } 49 | 50 | if (!data || !data.error) { 51 | return Promise.reject(new RTError(500, 'HttpError')); 52 | } 53 | return Promise.reject(new RTError(400, 'ModuleError', data.error.message || data.error)); 54 | } 55 | return data; 56 | }, 57 | }); 58 | } 59 | 60 | async refreshToken({ client_id, client_secret, refresh_token }) { 61 | const o = GoogleDrive.oauth2s[0]; 62 | const data = await this.service.post( 63 | 'https://www.googleapis.com/oauth2/v4/token', 64 | new URLSearchParams({ 65 | client_id: client_id || o.client_id, 66 | client_secret: client_secret || o.client_secret, 67 | grant_type: 'refresh_token', 68 | refresh_token, 69 | }) 70 | ); 71 | logger.log('google drive access_token:' + data.access_token); 72 | return data; 73 | } 74 | 75 | async findChildItem(pid, name) { 76 | return this.service 77 | .get('drive/v3/files', { 78 | params: { 79 | includeItemsFromAllDrives: true, 80 | supportsAllDrives: true, 81 | q: `name = '${name}' and '${pid}' in parents and trashed = false`, 82 | orderBy: 'folder,name,modifiedTime desc', 83 | fields: 'files(id,name,mimeType,size,modifiedTime,shortcutDetails),nextPageToken', 84 | pageSize: 10, 85 | }, 86 | }) 87 | .then((data) => data.files.map(filter).find((e) => e.name === name)); 88 | } 89 | 90 | async itemInfo(id) { 91 | return this.service 92 | .get(`drive/v3/files/${id}`, { 93 | params: { 94 | supportsAllDrives: true, 95 | fields: 'id,name,mimeType,size,modifiedTime,shortcutDetails', 96 | }, 97 | }) 98 | .then(filter); 99 | } 100 | 101 | async fetchList(parentId, pageToken) { 102 | const params = { 103 | includeItemsFromAllDrives: true, 104 | supportsAllDrives: true, 105 | q: `'${parentId}' in parents and trashed = false`, 106 | orderBy: 'folder,name,modifiedTime desc', 107 | fields: 'files(id,name,mimeType,size,modifiedTime,shortcutDetails,webContentLink,thumbnailLink),nextPageToken', 108 | pageSize: PAGE_SIZE, 109 | }; 110 | if (pageToken) { 111 | params.pageToken = pageToken; 112 | } 113 | return this.service.get('drive/v3/files', { params }).then((data) => ({ 114 | list: data.files.map(filter), 115 | next: data.nextPageToken, 116 | })); 117 | } 118 | 119 | downInfo(id) { 120 | return { 121 | url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media&supportsAllDrives=true`, 122 | headers: { 123 | Authorization: 'Bearer ' + this.access_token, 124 | }, 125 | }; 126 | } 127 | } 128 | 129 | GoogleDrive.oauth2s = [ 130 | { 131 | client_id: '202264815644.apps.googleusercontent.com', 132 | client_secret: 'X4Z3ca8xfWDb1Voo-F9a7ZxJ', 133 | redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', 134 | }, 135 | { 136 | client_id: '695250577395-08nocpbl8suogn56vjlpmifnhp5a4d7e.apps.googleusercontent.com', 137 | client_secret: 'k8xsOAGqhcmF1peWUDhZOeCK', 138 | redirect_uri: 'https://point.onesrc.cn/oauth2', 139 | }, 140 | ]; 141 | 142 | module.exports = { 143 | get params() { 144 | return [ 145 | _P('refresh_token', '', '获取refresh_token', 7, '', false, true), 146 | _P('root', '', '默认为根目录;如果想使用子目录,请填写目录id;如果想使用团队盘,请使用团队盘id', 5, 'root', false, false), 147 | _P('client_id', '', '', 5, '', false, false), 148 | _P('client_secret', '', '', 5, '', false, false), 149 | ]; 150 | }, 151 | async handle(config, data, cache, ctx) { 152 | let $m = cache.$m || {}; 153 | ctx.assert($m.isValid || (config.refresh_token && (cache.$m = $m = await GoogleDrive.build(config))), 400, 'ConfigError', { fields: ['refresh_token'] }); 154 | data.id = data.id || (await $m.getIDByPath(data.path)); 155 | return this[data.command](data, cache, ctx); 156 | }, 157 | async ls({ path, id, page }, { $m }, ctx) { 158 | if (path.endsWith('/')) { 159 | const { list, next } = await $m.fetchList(id, page); 160 | ctx.assert(list.length > 0 || (await $m.itemInfo(id)).type === 1, 400, 'ItemIsFile'); 161 | ctx.respondList(list, next); 162 | } else { 163 | const e = await $m.itemInfo(id); 164 | ctx.respondOne(e, e.type ? null : $m.downInfo(e.id)); 165 | } 166 | }, 167 | }; 168 | -------------------------------------------------------------------------------- /lib/routers/node_fs.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const fs = require('fs'); 3 | const { _P, path: _path } = require('../utils/node'); 4 | 5 | const { PAGE_SIZE: PAGE } = require('../conf/sys-config'); 6 | 7 | module.exports = { 8 | params() { 9 | return [_P('root', '', '', 5, '', false, false)]; 10 | }, 11 | async handle(config, data, cache, ctx) { 12 | if (!cache.flag) { 13 | if (!config.root) { 14 | config.root = ''; 15 | } else if (config.root.endsWith('/')) { 16 | config.root = config.root.slice(0, -1); 17 | } 18 | cache.flag = true; 19 | } 20 | data.path = config.root + data.path; 21 | ctx.assert(fs.existsSync(data.path), 404, 'ItemNotExist'); 22 | return this.ls(data, cache, ctx); 23 | }, 24 | async ls({ path, page }, _1, ctx) { 25 | const stats = fs.statSync(path); 26 | if (stats.isDirectory()) { 27 | if (!path.endsWith('/')) { 28 | ctx.respondOne({ 29 | type: 1, 30 | name: _path.basename(path), 31 | size: process.platform === 'win32' ? null : stats.size, 32 | time: new Date(stats.mtime).toISOString(), 33 | }); 34 | return; 35 | } // 可 36 | page = Number(page && page.slice(1)) || 0; 37 | const list = await Promise.all( 38 | fs.readdirSync(path).map( 39 | (fileName) => 40 | new Promise((resolve) => { 41 | fs.stat(path + fileName, (err, st) => { 42 | if (err) { 43 | logger.warn(path + ':' + fileName + ', ' + err.message); 44 | resolve(null); 45 | } else if (st.isDirectory()) { 46 | resolve({ 47 | type: 1, 48 | name: fileName, 49 | size: process.platform === 'win32' ? null : st.size, 50 | time: new Date(st.mtime).toISOString(), 51 | }); 52 | } else if (st.isFile()) { 53 | resolve({ 54 | type: 0, 55 | name: fileName, 56 | size: st.size, 57 | time: new Date(st.mtime).toISOString(), 58 | }); 59 | } else { 60 | resolve(null); 61 | } 62 | }); 63 | }) 64 | ) 65 | ); 66 | ctx.respondList(list.filter((e) => e).slice(page * PAGE, page * PAGE + PAGE), page * PAGE + PAGE < list.length ? 'l' + (page + 1) : null); 67 | } else if (stats.isFile()) { 68 | ctx.assert(!path.endsWith('/'), 403, 'ItemIsFile'); 69 | ctx.respondOne( 70 | { 71 | type: 0, 72 | name: _path.basename(path), 73 | size: stats.size, 74 | time: new Date(stats.mtime).toISOString(), 75 | }, 76 | { url: 'file://' + path } 77 | ); 78 | } else { 79 | ctx.throw(404, 'ItemNotExist'); 80 | } 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /lib/routers/onedrive.js: -------------------------------------------------------------------------------- 1 | const { request, RTError } = require('../utils/node'); 2 | const logger = require('../utils/logger'); 3 | const { _P, deleteAttributes } = require('../utils/node'); 4 | const op = require('../core/op'); 5 | 6 | const { PAGE_SIZE } = require('../conf/sys-config'); 7 | 8 | function filter(e) { 9 | const res = { 10 | type: 1, 11 | name: e.name, 12 | size: e.size, 13 | mime: '', 14 | time: e.lastModifiedDateTime, 15 | }; 16 | if (e.file) { 17 | res.type = 0; 18 | res.mime = e.file.mimeType; 19 | res.url = e['@microsoft.graph.downloadUrl'] || e['@content.downloadUrl'] || null; 20 | } 21 | return res; 22 | } 23 | 24 | module.exports = { 25 | get params() { 26 | return [ 27 | _P('type', 'com', 'com:国际版 cn:世纪互联特供版', 7, ['com', 'cn'], false, true), 28 | _P('refresh_token', '', '获取refresh_token', 7, '', true, true), 29 | _P('share_url', '', 'OneDrive分享链接', 7, 'https://{?}-my.sharepoint.com/:f:/g/personal/{?}/{?}', false, true), 30 | _P('root', '', '', 5, '/', false, false), 31 | _P('api_url', '', 'sharepoint 使用此项', 5, 'https://graph.microsoft.com/v1.0/sites/{site-id}/drive/', false, false), 32 | _P('client_id', '', '', 5, '', false, false), 33 | _P('client_secret', '', '', 5, '', false, false), 34 | _P('refresh_etime', Date.now() + 3600 * 24 * 10 + '', '', 1, { hidden: true }, false, false), 35 | ]; 36 | }, 37 | 38 | async handle(config, data, cache, ctx) { 39 | // 逻辑略显臃肿 为了统一所有的onedrive 40 | if ((cache.etime || 0) < Date.now()) { 41 | if (!config.root) { 42 | config.root = ''; 43 | } else if (config.root.endsWith('/')) { 44 | config.root = config.root.slice(0, -1); 45 | } 46 | // 控制,删除无用属性 type 默认为com 47 | if (config.type !== 'cn') { 48 | deleteAttributes(config, ['type']); 49 | } 50 | 51 | if (config.share_url) { 52 | deleteAttributes(config, ['refresh_token', 'api_url', 'client_id', 'client_secret', 'refresh_etime']); 53 | if (config.type === 'cn') { 54 | cache.$point = await SharePoint.build(config.share_url); 55 | } else { 56 | cache.$one = await OneDrive.build(config); 57 | } 58 | } else if (config.refresh_token) { 59 | cache.$one = await OneDrive.build(config); 60 | deleteAttributes(config, ['share_url']); 61 | if ((config.refresh_etime || 0) < Date.now()) { 62 | config.refresh_etime = Date.now() + 30 * 24 * 3600 * 1000; 63 | config.refresh_token = cache.$one.refresh_token; 64 | await op.saveConfig('onedrive refresh_token auto save'); 65 | } 66 | } else { 67 | throw new RTError(400, 'ConfigError', { fields: ['refresh_token', 'share_url'] }); 68 | } 69 | cache.etime = Date.now() + 3600 * 1000; 70 | } 71 | if (config.root) { 72 | data.path = config.root + data.path; 73 | if (data.desPath) { 74 | data.desPath = config.root + data.desPath; 75 | } 76 | } 77 | if (cache.$point) { 78 | ctx.assert(data.command === 'ls', 403, 'CommandNotAllowed', { command: data.command }); 79 | return this.ls_cn(data, cache, ctx); 80 | } else { 81 | return this[data.command](data, cache, ctx); 82 | } 83 | }, 84 | 85 | async ls({ path, page }, { $one }, ctx) { 86 | if (!path.endsWith('/')) { 87 | // 处理文件情况 88 | ctx.respondOne(filter(await $one.itemInfo(path))); 89 | } else { 90 | const data = await $one.fetchItems(path === '/' ? path : path.slice(0, -1), page); 91 | ctx.respondList(data.value.map(filter), data['@odata.nextLink'] ? /skiptoken=(\w*)/.exec(data['@odata.nextLink'])[1] : null); 92 | } 93 | }, 94 | 95 | // @Todo page 96 | 97 | async ls_cn({ path }, { $point }, ctx) { 98 | const data = await $point.spListData(path); 99 | const offset = (new Date().getTimezoneOffset() - data.RegionalSettingsTimeZoneBias || 0) * 3600000; 100 | if (path.endsWith('/')) { 101 | // 文件夹 102 | ctx.respondList( 103 | data.ListData.Row.map((e) => ({ 104 | type: Number(e.FSObjType), 105 | name: e.LinkFilename, 106 | size: Number(e.SMTotalFileStreamSize), 107 | time: new Date(new Date(e.SMLastModifiedDate) - offset).toISOString(), 108 | })) 109 | ); 110 | } else { 111 | const info = await $point.spGetItemInfo(data.ListData.CurrentFolderSpItemUrl); 112 | ctx.respondOne({ 113 | type: info.file ? 0 : 1, 114 | name: info.name, 115 | size: Number(info.size), 116 | time: new Date(new Date(info.lastModifiedDateTime) - offset).toISOString(), 117 | url: info['@content.downloadUrl'], 118 | }); 119 | } 120 | }, 121 | 122 | async mkdir({ path, name }, { $one }) { 123 | await $one.mkdir(path, name); 124 | }, 125 | async mv({ path, desPath }, { $one }) { 126 | await $one.move(path, desPath); 127 | }, 128 | async cp({ path, desPath }, { $one }) { 129 | await $one.copy(path, desPath); 130 | }, 131 | async rm({ path }, { $one }, ctx) { 132 | await $one.delete(path); 133 | ctx.respond(204); 134 | }, 135 | async ren({ path, name }, { $one }) { 136 | await $one.rename(path, name); 137 | }, 138 | async touch({ path, name, content, mime }, { $one }, ctx) { 139 | await $one.touch(path, name, content, mime); 140 | ctx.respond(201); 141 | }, 142 | async upload({ path, name, size }, { $one }, ctx) { 143 | ctx.respond(201, await $one.uploadSession(path, name, size)); 144 | }, 145 | }; 146 | 147 | class OneDrive { 148 | static async build(config) { 149 | const o = new OneDrive(config); 150 | const { api_url, access_token, refresh_token } = config.share_url ? await SharePoint.getAccessToken(config.share_url) : await o.getAccessToken(config); 151 | o.api_url = api_url; 152 | o.access_token = access_token; 153 | o.refresh_token = refresh_token; 154 | o.service.defaults.baseURL = api_url; 155 | o.service.defaults.headers.Authorization = 'Bearer ' + access_token; 156 | return o; 157 | } 158 | 159 | async getAccessToken({ refresh_token, type, api_url, client_id, client_secret }) { 160 | const o = OneDrive.oauth2s[type] || OneDrive.oauth2s.com; 161 | api_url = api_url || o.api_url; 162 | return this.service 163 | .post( 164 | o.oauth_url + 'token', 165 | new URLSearchParams({ 166 | client_id: client_id || o.client_id, 167 | client_secret: client_secret || o.client_secret, 168 | grant_type: 'refresh_token', 169 | requested_token_use: 'on_behalf_of', 170 | refresh_token: refresh_token, 171 | }) 172 | ) 173 | .then((data) => (data.api_url = api_url) && data); 174 | } 175 | 176 | constructor() { 177 | this.service = request.create({ 178 | onResponse: (res) => { 179 | const { status, data } = res; 180 | if (status >= 300) { 181 | if (status === 404) { 182 | return Promise.reject(new RTError(404, 'ItemNotExist')); 183 | } 184 | 185 | if (!data || !data.error) { 186 | return Promise.reject(new RTError(500, 'HttpError')); 187 | } 188 | 189 | if (data.error_description) { 190 | return Promise.reject(new RTError(400, 'ModuleError', data.error + ':' + data.error_description)); 191 | } else { 192 | return Promise.reject(new RTError(400, 'ModuleError', data.error.code + ':' + data.error.message)); 193 | } 194 | } 195 | return data; 196 | }, 197 | }); 198 | } 199 | 200 | async itemInfo(path) { 201 | const data = await this.service.get('root' + (path === '/' ? '' : ':' + encodeURI(path))); 202 | logger.log(path + ':' + data.id); 203 | return data; 204 | } 205 | 206 | async getIdByPath(path) { 207 | return this.itemInfo(path).then(({ id }) => id); 208 | } 209 | 210 | async fetchItems(path, pageToken) { 211 | const params = { $top: PAGE_SIZE }; 212 | if (pageToken) { 213 | params.$skiptoken = pageToken; 214 | } 215 | return this.service.get('root' + (path === '/' ? '' : ':' + encodeURI(path) + ':') + '/children', { params }); 216 | } 217 | 218 | async mkdir(path, name) { 219 | return this.service.post('items/' + (await this.getIdByPath(path)) + '/children', { 220 | name: name, 221 | folder: {}, 222 | '@microsoft.graph.conflictBehavior': 'fail', 223 | }); 224 | } 225 | 226 | async move(path, desPath) { 227 | return this.service.patch('items/' + (await this.getIdByPath(path)), { 228 | parentReference: { 229 | id: await this.getIdByPath(desPath), 230 | }, 231 | }); 232 | } 233 | 234 | async copy(path, desPath) { 235 | return this.service.post('items/' + (await this.getIdByPath(path)) + '/copy', { 236 | parentReference: { 237 | id: await this.getIdByPath(desPath), 238 | }, 239 | }); 240 | } 241 | 242 | async delete(path) { 243 | return this.service.delete('items/' + (await this.getIdByPath(path))); 244 | } 245 | 246 | async rename(path, name) { 247 | return this.service.patch('items/' + (await this.getIdByPath(path)), { name }); 248 | } 249 | 250 | async touch(path, name, content, mime) { 251 | return this.service.put('items/' + (await this.getIdByPath(path)) + ':/' + name + ':/content', content, { 252 | headers: { 253 | 'Content-Type': mime, 254 | }, 255 | }); 256 | } 257 | 258 | async uploadSession(path, name) { 259 | return this.service.post('root:' + encodeURI(path + name) + ':/createUploadSession', { 260 | item: { 261 | '@microsoft.graph.conflictBehavior': 'fail', 262 | }, 263 | }); 264 | } 265 | } 266 | 267 | OneDrive.oauth2s = { 268 | com: { 269 | client_id: 'ca39c9ea-01b7-4199-b663-07cc3406196c', 270 | client_secret: 'AVMUwY_9_K8CbCXltBnNVi1~-5v6cM8qt6', 271 | oauth_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/', 272 | api_url: 'https://graph.microsoft.com/v1.0/me/drive/', 273 | }, 274 | cn: { 275 | client_id: '320ca2f3-9411-401e-99df-bcf163561733', 276 | client_secret: 'VHTu]JW?m5qQxER]klkks9XHRY]y8Et0', 277 | oauth_url: 'https://login.partner.microsoftonline.cn/common/oauth2/v2.0/', 278 | api_url: 'https://microsoftgraph.chinacloudapi.cn/v1.0/me/drive/', 279 | }, 280 | }; 281 | 282 | class SharePoint { 283 | static async build(share_url) { 284 | const o = new SharePoint(); 285 | return o.init(share_url); 286 | } 287 | 288 | static async getAccessToken(share_url) { 289 | const point = await SharePoint.build(share_url); 290 | const data = await point.spListData('/'); 291 | if (data.ListSchema && data.ListSchema['.driveUrl']) { 292 | return { 293 | api_url: data.ListSchema['.driveUrl'] + '/', 294 | access_token: data.ListSchema['.driveAccessToken'].slice('access_token='.length), 295 | }; 296 | } else { 297 | throw new RTError(500, 'ConfigError', { fields: ['share_url'] }); 298 | } 299 | } 300 | 301 | async init(share_url) { 302 | const match = /https:\/\/(?[^/]*)\/:f:\/g\/personal\/(?[^/]*).*/.exec(share_url); 303 | this.cookie = await this.getCookie(match[0]); 304 | this.origin = match.groups.origin; 305 | this.account = match.groups.account; 306 | return this; 307 | } 308 | 309 | async getCookie(shareUrl) { 310 | const config = { 311 | maxRedirects: 0, 312 | validateStatus: function (status) { 313 | return status >= 200 && status < 400; 314 | }, 315 | headers: { 316 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0', 317 | Cookie: '', 318 | }, 319 | }; 320 | const { headers } = await request.get(shareUrl, config); 321 | if (!headers['set-cookie'] || !headers['set-cookie'][0]) { 322 | throw new RTError(500, 'ModuleError', 'This sharing link has been canceled'); 323 | } 324 | logger.log('sharepoint cookie:' + headers['set-cookie'][0]); 325 | return headers['set-cookie'][0]; 326 | } 327 | 328 | async spListData(path) { 329 | const url = `https://${this.origin}/personal/${this.account}/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream`; 330 | const config = { 331 | headers: { 332 | origin: 'https://' + this.origin, 333 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0', 334 | Cookie: this.cookie, 335 | }, 336 | params: { 337 | '@a1': `'/personal/${this.account}/Documents'`, 338 | RootFolder: `/personal/${this.account}/Documents${path}`, 339 | TryNewExperienceSingle: 'TRUE', 340 | }, 341 | }; 342 | const data = { 343 | parameters: { 344 | ViewXml: ` 345 | 346 | 347 | 348 | 349 | 350 | 351 | ${PAGE_SIZE}`, 352 | RenderOptions: 136967, 353 | AllowMultipleValueFilterForTaxonomyFields: true, 354 | AddRequiredFields: true, 355 | }, 356 | }; 357 | const res = await request.post(url, data, config); 358 | return res.data; 359 | } 360 | 361 | async spGetItemInfo(spItemUrl) { 362 | const config = { 363 | headers: { 364 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0', 365 | Cookie: this.cookie, 366 | }, 367 | }; 368 | const res = await request.get(spItemUrl, config); 369 | return res.data; 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /lib/routers/phony.js: -------------------------------------------------------------------------------- 1 | const { _P } = require('../utils/node'); 2 | module.exports = { 3 | get params() { 4 | return [_P('nothing', '', '文件挂载专用,可用于补充挂载文件,请不要填写此选项', 7, '', false, false)]; 5 | }, 6 | async handle(_, data, cache, ctx) { 7 | return this[data.command](data, cache, ctx); 8 | }, 9 | async ls({ path }, _, ctx) { 10 | ctx.assert(path === '/', 404, 'ItemNotExist'); 11 | ctx.respondList([]); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /lib/routers/teambition.js: -------------------------------------------------------------------------------- 1 | const { request, RTError, IDHelper, _P } = require('../utils/node'); 2 | const { PAGE_SIZE } = require('../conf/sys-config'); 3 | 4 | function folderFilter(e) { 5 | return { 6 | type: 1, 7 | name: e.title, 8 | time: e.updated, 9 | size: null, 10 | id: e._id, 11 | }; 12 | } 13 | 14 | function fileFilter(e) { 15 | return { 16 | type: 0, 17 | name: e.fileName, 18 | time: e.updated, 19 | size: e.fileSize, 20 | id: e._id, 21 | url: e.downloadUrl, 22 | }; 23 | } 24 | 25 | class Teambition extends IDHelper { 26 | constructor({ parentId, projectId, cookie }) { 27 | super(parentId); 28 | this._projectId = projectId; 29 | this.service = request.create({ 30 | baseURL: 'https://www.teambition.com/api/', 31 | headers: { cookie }, 32 | onResponse: (res) => { 33 | const { status, data } = res; 34 | if (status >= 300) { 35 | if (status === 404) { 36 | return Promise.reject(new RTError(404, 'ItemNotExist')); 37 | } 38 | 39 | if (!data || !data.message) { 40 | return Promise.reject(new RTError(500, 'HttpError')); 41 | } 42 | 43 | return Promise.reject(new RTError(400, 'ModuleError', data.message)); 44 | } 45 | return data; 46 | }, 47 | }); 48 | } 49 | 50 | async findChildItem(pid, name) { 51 | let e = await this.fetchFolders(pid, 1, 500).then((arr) => arr.find((e) => e.name === name)); 52 | if (!e) { 53 | e = await this.fetchFiles(pid, 1, 1000).then((arr) => arr.find((e) => e.name === name)); 54 | if (!e) { 55 | throw new RTError(404, 'ItemNotExist'); 56 | } 57 | } 58 | e.pid = pid; 59 | return e; 60 | } 61 | 62 | async fetchFiles(pid, page = 1, count = PAGE_SIZE) { 63 | return this.service 64 | .get(`works`, { 65 | params: { 66 | _parentId: pid, 67 | _projectId: this._projectId, 68 | order: 'nameAsc', 69 | count: count, 70 | page: page, 71 | }, 72 | }) 73 | .then((data) => data.map(fileFilter)); 74 | } 75 | 76 | async fetchFolders(pid, page = 1, count = 200) { 77 | return this.service 78 | .get(`collections`, { 79 | params: { 80 | _parentId: pid, 81 | _projectId: this._projectId, 82 | order: 'nameAsc', 83 | count: count, 84 | page: page, 85 | }, 86 | }) 87 | .then((data) => data.map(folderFilter).filter((e) => e.name)); 88 | } 89 | 90 | async fetchList(pid, page = 1) { 91 | if (page === 1) { 92 | const folders = await this.fetchFolders(pid); 93 | const files = await this.fetchFiles(pid); 94 | const next = files.length === PAGE_SIZE ? 2 : null; 95 | return { list: folders.concat(files), next }; 96 | } else { 97 | const list = await this.fetchFiles(pid, page); 98 | // 尽可能提高准确性 99 | const next = list.length === PAGE_SIZE ? page + 1 : null; 100 | return { list, next }; 101 | } 102 | } 103 | 104 | async fileInfo(id) { 105 | return this.service.get('works/' + id).then(fileFilter); 106 | } 107 | 108 | async folderInfo(id) { 109 | return this.service.get('collections/' + id).then(folderFilter); 110 | } 111 | } 112 | 113 | module.exports = { 114 | params() { 115 | return [_P('cookie', '', '形如TEAMBITION_SESSIONID=*; TEAMBITION_SESSIONID.sig=*', 7, '', true, true), _P('projectId', '', '项目id', 7, '', false, true), _P('parentId', '', '根目录或文件夹id', 7, '', false, true)]; 116 | }, 117 | async handle(config, data, cache, ctx) { 118 | if ((cache.etime || 0) < Date.now()) { 119 | if (!config.cookie || !config.projectId || !config.parentId) { 120 | throw new RTError(400, 'ConfigError', { fields: ['cookie', 'projectId', 'parentId'] }); 121 | } 122 | cache.$t = new Teambition(config); 123 | cache.etime = Date.now() + 3600 * 1000; 124 | } 125 | if (!data.id) { 126 | data.id = await cache.$t.getIDByPath(data.path); 127 | } 128 | return this.ls(data, cache, ctx); 129 | }, 130 | async ls({ path, id, page = 1 }, { $t }, ctx) { 131 | if (path.endsWith('/')) { 132 | await $t 133 | .fetchList(id, page) 134 | .then(({ list, next }) => { 135 | ctx.respondList(list, next); 136 | }) 137 | .catch(async (err) => { 138 | if (err.type === 'ItemNotExist') { 139 | // 不报错则是文件 报错正常退出 140 | await $t.fileInfo(id); 141 | throw new RTError(400, 'ItemIsFile'); 142 | } 143 | }); 144 | } else { 145 | await $t 146 | .folderInfo(id) 147 | .catch((err) => { 148 | if (err.type === 'ItemNotExist') { 149 | return $t.fileInfo(id); 150 | } 151 | }) 152 | .then((e) => { 153 | const o = Object.assign({}, e); 154 | delete o.pid; 155 | ctx.respondOne(o); 156 | }); 157 | } 158 | }, 159 | }; 160 | -------------------------------------------------------------------------------- /lib/starters/cf-worker.js: -------------------------------------------------------------------------------- 1 | globalThis.module = {}; 2 | globalThis.require = () => 0; 3 | globalThis.__dirname = ''; 4 | 5 | const app = require('../app'); 6 | const { P, exposeHeadersWhenProxy, request: _request } = require('../utils/node'); 7 | 8 | //otherwise, Date.now() is 0 9 | function initApp() { 10 | initApp = () => 0; 11 | 12 | const KVConfig = typeof OPCONFIG === 'undefined' ? null : OPCONFIG; 13 | 14 | function ensureConfigExist() { 15 | if (typeof KVConfig === 'undefined') { 16 | throw new Error('KV Namespace not found'); 17 | } 18 | } 19 | 20 | async function readConfig() { 21 | return KVConfig ? JSON.parse((await KVConfig.get('op-config')) || '{}') : {}; 22 | } 23 | 24 | async function writeConfig(config) { 25 | ensureConfigExist(); 26 | return KVConfig.put('op-config', JSON.stringify(config)); 27 | } 28 | 29 | const h = '注意,必需新建KV,并将其绑定到对应的 Workers KV Namespace,绑定的名称为OPCONFIG'; 30 | 31 | app.initialize({ 32 | name: 'cf-worker', 33 | readConfig, 34 | writeConfig, 35 | params: [P('x_empty', '', h, 8, 'just let me empty', false, false)], 36 | }); 37 | } 38 | 39 | async function handleEvent(event) { 40 | initApp(); 41 | const request = event.request; 42 | const url = new URL(request.url); 43 | const h = {}; 44 | for (let [k, v] of request.headers) { 45 | h[k] = v; 46 | } 47 | const req = { 48 | method: request.method, 49 | path: url.pathname, 50 | headers: h, 51 | body: await request.text(), 52 | query: url.search, 53 | ip: [request.headers.get('CF-Connecting-IP')], 54 | }; 55 | return app.handleRequest(req).then((response) => { 56 | if (response.callback_down) { 57 | const { url, method, headers = {}, body } = response.callback_down; 58 | h.range && (headers.range = h.range); 59 | return _request.request({ url, method, headers, body, responseType: 'stream' }).then((r) => { 60 | const h2 = response.headers; 61 | Object.entries(r.headers).forEach(([k, v]) => exposeHeadersWhenProxy(k) && (h2[k] = v)); 62 | return new Response(r.data, { 63 | status: r.status, 64 | headers: h2, 65 | }); 66 | }); 67 | } 68 | return new Response(response.body, { 69 | status: response.status, 70 | headers: response.headers, 71 | }); 72 | }); 73 | } 74 | 75 | addEventListener('fetch', (event) => { 76 | event.respondWith( 77 | handleEvent(event).catch((err) => { 78 | return new Response( 79 | JSON.stringify({ 80 | error: err.type || 'UnknownError', 81 | data: err.data || {}, 82 | msg: err.message, 83 | }), 84 | { 85 | status: err.status || 500, 86 | headers: { 87 | 'access-control-allow-origin': '*', 88 | 'content-type': 'application/json', 89 | }, 90 | } 91 | ); 92 | }) 93 | ); 94 | }); 95 | -------------------------------------------------------------------------------- /lib/starters/local-test.js: -------------------------------------------------------------------------------- 1 | require('../conf/sys-config').NODE_ENV = 'dev'; 2 | require('../conf/sys-config').PORT = 80; 3 | require('../utils/node').request.defaults.proxy = { 4 | host: '127.0.0.1', 5 | port: '8899', 6 | }; 7 | const path = require('path'); 8 | const pathPrefix = path.resolve(__dirname, '../views/art/') + '/'; 9 | const op = require('../core/op'); 10 | Object.values(op.themes).forEach((v) => { 11 | if (v.type === 'art') { 12 | const p = pathPrefix + v.name; 13 | v.render = (t) => require('art-template')(p, t); 14 | } 15 | }); 16 | require('./node-http'); 17 | const fs = require('fs'); 18 | const p = path.resolve(__dirname, '../../tmp/op-config.json'); 19 | 20 | require('../views/art2js')(true); 21 | 22 | require('../core/op').initialize({ 23 | name: 'node-http', 24 | async readConfig() { 25 | return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}; 26 | }, 27 | async writeConfig(config) { 28 | fs.writeFileSync(p, JSON.stringify(config, null, 2)); 29 | }, 30 | params: [], 31 | }); 32 | -------------------------------------------------------------------------------- /lib/starters/node-http.js: -------------------------------------------------------------------------------- 1 | const { PORT, NODE_ENV } = require('../conf/sys-config'); 2 | process.env.NODE_ENV = NODE_ENV; 3 | 4 | const http = require('http'); 5 | const app = require('../app'); 6 | const logger = require('../utils/logger'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const { RTError, P, mime, request, exposeHeadersWhenProxy } = require('../utils/node'); 10 | 11 | const CONFIG_FILE_PATHS = [path.resolve('/', 'etc/op-config.json'), path.resolve(__dirname, '../conf/op-config.json')]; 12 | 13 | async function readConfig() { 14 | for (const path of CONFIG_FILE_PATHS) { 15 | if (fs.existsSync(path)) { 16 | logger.log('read config from:' + path); 17 | return JSON.parse(fs.readFileSync(path, 'utf8')); 18 | } 19 | } 20 | return {}; 21 | } 22 | 23 | async function writeConfig(config, { x_node_config_path: config_file_path }) { 24 | if (!CONFIG_FILE_PATHS.includes(config_file_path)) { 25 | throw new Error('config-file-path is invalid: ' + config_file_path); 26 | } 27 | 28 | for (const path of CONFIG_FILE_PATHS) { 29 | if (config_file_path === path) { 30 | break; 31 | } 32 | if (fs.existsSync(path)) { 33 | fs.unlinkSync(path); 34 | } 35 | } 36 | fs.writeFileSync(config_file_path, JSON.stringify(config, null, 2)); 37 | } 38 | 39 | app.initialize({ 40 | name: 'node-http', 41 | readConfig, 42 | writeConfig, 43 | params: [P('x_node_config_path', CONFIG_FILE_PATHS[1], '配置文件存放位置', 8, CONFIG_FILE_PATHS, false, true)], 44 | }); 45 | 46 | async function handleRequest(req, res) { 47 | const s = process.hrtime.bigint(); 48 | return new Promise((resolve, reject) => { 49 | const request = { 50 | method: req.method, 51 | path: req.url, 52 | headers: req.headers, 53 | body: '', 54 | query: req.url, 55 | ip: [req.connection.remoteAddress], 56 | baseURL: 'http://' + req.headers.host, 57 | }; 58 | if (req.method === 'PUT') { 59 | request.body = req; 60 | resolve(request); 61 | } else { 62 | const buffer = []; 63 | req.on('data', (chunk) => { 64 | buffer.push(chunk); 65 | }); 66 | req.on('end', () => { 67 | request.body = Buffer.concat(buffer).toString('utf8'); 68 | resolve(request); 69 | }); 70 | req.on('error', (err) => { 71 | reject(err); 72 | }); 73 | } 74 | }) 75 | .then((request) => app.handleRequest(request)) 76 | .then((response) => { 77 | if (response.callback_down) { 78 | const { url, method, headers = {}, body } = response.callback_down; 79 | req.headers.range && (headers.range = req.headers.range); 80 | if (url.startsWith('file:')) { 81 | const p = url.slice('file://'.length); 82 | // file download 83 | const stats = fs.statSync(p); 84 | if (!stats.isFile()) { 85 | throw new RTError(403, 'NotDownloadable', { name: path.basename(p) }); 86 | } 87 | const size = stats.size; 88 | const h = response.headers; 89 | h['accept-ranges'] = 'bytes'; 90 | h['content-type'] = mime.get(p); 91 | h['content-disposition'] = 'attachment; filename=' + encodeURIComponent(path.basename(p)); 92 | const reqHttpRange = headers.range; 93 | if (reqHttpRange) { 94 | const m = /bytes=(?\d*)-(?\d*)/.exec(reqHttpRange) || { groups: {} }; 95 | const start = Number(m.groups.start) || 0; 96 | const end = Number(m.groups.end) || Math.min(start + 1024 * 1024 * 5, size - 1); 97 | if (!(start <= end && end < size)) { 98 | throw new RTError(403, 'InvalidHttpRange', { range: reqHttpRange, size }); 99 | } 100 | h['content-length'] = end - start + 1; 101 | h['content-range'] = `bytes ${start}-${end}/${size}`; 102 | res.writeHead(206, h); 103 | fs.createReadStream(p, { start, end }).pipe(res); 104 | } else { 105 | h['content-length'] = size; 106 | res.writeHead(200, h); 107 | fs.createReadStream(p).pipe(res); 108 | } 109 | } else { 110 | // http 111 | return request.request({ url, method, headers, body, responseType: 'stream' }).then((r) => { 112 | const h2 = response.headers; 113 | Object.entries(r.headers).forEach(([k, v]) => exposeHeadersWhenProxy(k) && (h2[k] = v)); 114 | res.writeHead(r.status, h2); 115 | r.data.pipe(res); 116 | }); 117 | } 118 | logger.debug('stream: ' + url + ' ' + headers.range); 119 | } else { 120 | res.writeHead(response.status, response.headers); 121 | res.end(response.body); 122 | logger.debug(`time consume:${Number(process.hrtime.bigint() - s) / 1000000}`); 123 | } 124 | }) 125 | .catch((err) => { 126 | res.writeHead(err.status || 500, { 127 | 'access-control-allow-origin': '*', 128 | 'content-type': 'application/json', 129 | }); 130 | res.end( 131 | JSON.stringify({ 132 | error: err.type || 'UnknownError', 133 | data: err.data || {}, 134 | message: err.message, 135 | }) 136 | ); 137 | }); 138 | } 139 | 140 | const server = http.createServer(handleRequest).listen(PORT); 141 | 142 | if (server.address()) { 143 | logger.log(`Running on ${process.platform}, port:${server.address().port}`); 144 | Object.values(require('os').networkInterfaces()) 145 | .map((e) => (e[0].family === 'IPv6' ? e[1].address : e[0].address)) 146 | .filter((e) => e) 147 | .sort() 148 | .forEach((e) => logger.log(`http://${e}:${PORT}`)); 149 | } 150 | -------------------------------------------------------------------------------- /lib/starters/tencent-scf.js: -------------------------------------------------------------------------------- 1 | const app = require('../app'); 2 | 3 | const COS = require('cos-nodejs-sdk-v5'); 4 | const { P } = require('../utils/node'); 5 | 6 | const env = process.env; 7 | const cosConfig = { 8 | SecretId: env.COS_SECRETID, 9 | SecretKey: env.COS_SECRETKEY, 10 | Bucket: env.COS_BUCKET, 11 | Region: env.COS_REGIN, 12 | Key: env.COS_KEY || 'op-config.json', 13 | }; 14 | 15 | const cos = new COS({ 16 | SecretId: cosConfig.SecretId, 17 | SecretKey: cosConfig.SecretKey, 18 | }); 19 | 20 | async function readConfig() { 21 | return cosConfig.SecretId 22 | ? new Promise((resolve, reject) => { 23 | cos.getObject( 24 | { 25 | Bucket: cosConfig.Bucket, 26 | Region: cosConfig.Region, 27 | Key: cosConfig.Key, 28 | }, 29 | function (err, data) { 30 | if (err) { 31 | reject(err); 32 | } else { 33 | resolve(JSON.parse(String(data.Body))); 34 | } 35 | } 36 | ); 37 | }) 38 | : {}; 39 | } 40 | 41 | async function writeConfig(config) { 42 | if (!cosConfig.SecretId) { 43 | throw new Error('未配置COS,无法读写配置'); 44 | } 45 | return new Promise((resolve, reject) => { 46 | cos.putObject( 47 | { 48 | Bucket: cosConfig.Bucket, 49 | Region: cosConfig.Region, 50 | Key: cosConfig.Key, 51 | Body: JSON.stringify(config), 52 | }, 53 | function (err) { 54 | if (err) { 55 | reject(err); 56 | } else { 57 | resolve(); 58 | } 59 | } 60 | ); 61 | }); 62 | } 63 | 64 | const h = '受限于腾讯云函数,必须在环境变量中添加 COS_SECRETID,COS_SECRETKEY,COS_BUCKET,COS_REGIN四个变量才能使用,详情自行查询腾讯云文档'; 65 | app.initialize({ 66 | name: 'scf', 67 | readConfig, 68 | writeConfig, 69 | params: [P('x_empty', '', h, 8, 'just let me empty', false, false)], 70 | }); 71 | 72 | exports.main_handler = async (event) => { 73 | event.headers['x-real-ip'] = event.requestContext.sourceIp; 74 | let p_12 = event.path; 75 | // 处理域名和路径,分离得到 p0 p12 76 | let requestContext_path = event.requestContext.path; 77 | if (requestContext_path.endsWith('/')) { 78 | requestContext_path = requestContext_path.slice(0, -1); 79 | } // / or /abc/ 80 | if (event.headers.host.startsWith(event.requestContext.serviceId)) { 81 | // 长域名 82 | event.headers['x-op-p0'] = `/${event.requestContext.stage}${requestContext_path}`; 83 | p_12 = p_12.slice(requestContext_path.length) || '/'; // 只有scf网关不规范 ,例如 /abc 前者才为 84 | } 85 | return await app.handleRequest({ 86 | method: event.httpMethod, 87 | path: p_12, 88 | headers: event.headers, 89 | body: event.body, 90 | query: event.queryString, 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /lib/starters/vercel-zeit.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const app = require('../app'); 3 | const { request, P } = require('../utils/node'); 4 | const logger = require('../utils/logger'); 5 | const op = require('../core/op'); 6 | 7 | let META; // process.env['VERCEL_URL']; 8 | 9 | async function readConfig() { 10 | return typeof CONFIG_OBJ === 'undefined' ? {} : CONFIG_OBJ; 11 | } 12 | 13 | async function writeConfig(config, { x_zeit_token, x_zeit_project_name }) { 14 | if (!x_zeit_token) { 15 | return Promise.reject(new Error('未配置 zeit token')); 16 | } 17 | const flag = await checkDeployment(x_zeit_token); 18 | if (op.config.version !== 1 && !flag) { 19 | return Promise.reject(new Error('lock! 之前已经提交过部署了,请等待生效后再试')); 20 | } 21 | const c = `const CONFIG_OBJ=${JSON.stringify(config)};const r3030958164335045=19218526256549961;\n`; 22 | let f = fs.readFileSync(__filename, 'utf-8').replace(/^const CONFIG_OBJ=.*;const r3030958164335045=19218526256549961;\n/, c); 23 | if (!f.startsWith('const CONFIG_OBJ=')) { 24 | f = c + f; 25 | } 26 | return request 27 | .post( 28 | 'https://api.vercel.com/v12/now/deployments', { 29 | name: x_zeit_project_name, 30 | files: [{ file: 'api/index.js', data: f }], 31 | target: 'production', 32 | meta: { last: META }, 33 | functions: { 'api/index.js': { maxDuration: 10 } }, 34 | routes: [{ src: '/.*', dest: 'api/index.js' }], 35 | projectSettings: { framework: null }, 36 | }, { 37 | headers: { 38 | Authorization: `Bearer ${x_zeit_token}`, 39 | }, 40 | } 41 | ) 42 | .then((d) => { 43 | logger.log(d); 44 | return (op.runtime.now = d.data.url); 45 | }); 46 | } 47 | 48 | async function checkDeployment(token) { 49 | return request 50 | .get('https://api.vercel.com/v5/now/deployments/', { 51 | headers: { 52 | Authorization: `Bearer ${token}`, 53 | }, 54 | params: { 55 | 'meta-last': META, 56 | }, 57 | }) 58 | .then((d) => { 59 | logger.log(d.request.header); 60 | logger.log(d.data.deployments.length); 61 | return d.data.deployments.length === 0; 62 | }); 63 | } 64 | 65 | app.initialize({ 66 | name: 'now.sh', 67 | readConfig, 68 | writeConfig, 69 | params: [ 70 | P('x_zeit_token', '', 'token', 8, '', false, true), 71 | P('x_zeit_project_name', 'onepoint', 'project name', 8, '', false, true) 72 | ] 73 | }); 74 | 75 | module.exports = async(req, res) => { 76 | try { 77 | if (META === undefined) { 78 | META = req.headers['x-vercel-deployment-url']; 79 | } 80 | req.path = req.url; 81 | const r = await app.handleRequest(req); 82 | res.writeHead(r.status, r.headers); 83 | res.end(r.body); 84 | } catch (err) { 85 | logger.log(err); 86 | res.writeHead(err.status || 500, { 87 | 'access-control-allow-origin': '*', 88 | 'content-type': 'application/json', 89 | }); 90 | res.end( 91 | JSON.stringify({ 92 | error: err.type || 'UnknownError', 93 | data: err.data || {}, 94 | message: err.message, 95 | }) 96 | ); 97 | } 98 | }; -------------------------------------------------------------------------------- /lib/utils/crypto-js-aes.js: -------------------------------------------------------------------------------- 1 | /* 2 | CryptoJS v3.1.2 3 | code.google.com/p/crypto-js 4 | (c) 2009-2013 by Jeff Mott. All rights reserved. 5 | code.google.com/p/crypto-js/wiki/License 6 | */ 7 | var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 8 | r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 9 | 32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, 10 | 2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, 11 | q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< 15 | l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); 16 | (function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, 17 | _doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), 18 | f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, 19 | m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, 20 | E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ 21 | 4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); 22 | (function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, 28 | this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, 29 | 1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, 30 | decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, 31 | b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); 32 | (function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, 33 | 16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> 34 | 8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= 35 | d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})(); 36 | 37 | module.exports = CryptoJS; -------------------------------------------------------------------------------- /lib/utils/error-message.js: -------------------------------------------------------------------------------- 1 | const e = { 2 | ItemNotExist({ path }) { 3 | return `文件(夹)${path || '?'}不存在`; 4 | }, 5 | Unauthorized({ field, type }) { 6 | let s = ''; 7 | if (type === 'empty') { 8 | s = '为空,请输入后重试'; 9 | } else if (type === 'invalid') { 10 | s = '已过期或不合法,请重新认证'; 11 | } else if (type === 'wrong') { 12 | s = '有误,请重新输入'; 13 | } 14 | return `字段${field}${s}`; 15 | }, 16 | CommandNotAllowed({ command }) { 17 | return `暂不支持${command}命令`; 18 | }, 19 | DriveNotExist({ path }) { 20 | return `路径${path}下未配置云盘`; 21 | }, 22 | ModuleNotExist({ module }) { 23 | return `模块${module}不存在`; 24 | }, 25 | InvalidPage({ page }) { 26 | return `分页参数${page}不合法`; 27 | }, 28 | ItemIsFile({ path }) { 29 | return `路径${path || '?'}对应一个文件,请注意path格式`; 30 | }, 31 | ConfigError({ fields }) { 32 | return `参数配置有误,请注意一下这些参数[${fields.toString()}]`; 33 | }, 34 | ModuleError(msg) { 35 | return `模块内部错误: ${msg || '???'}`; 36 | }, 37 | ReadError({ msg }) { 38 | return `配置读取失败: ${msg}`; 39 | }, 40 | SaveError({ msg }) { 41 | return `配置保存失败: ${msg}`; 42 | }, 43 | default(type) { 44 | return `发生错误${type || '?'}`; 45 | }, 46 | }; 47 | 48 | module.exports.parseErrorMsg = function (type, data) { 49 | return e[type] ? e[type](data) : e.default(type); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/utils/fetchAdapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // https://github.com/sgammon/axios/blob/feature/fetch/lib/adapters/fetch.js 4 | 5 | const settle = require('axios/lib/core/settle'); 6 | const buildURL = require('axios/lib/helpers/buildURL'); 7 | const buildFullPath = require('axios/lib/core/buildFullPath'); 8 | const createError = require('axios/lib/core/createError'); 9 | 10 | module.exports = function fetchAdapter(config) { 11 | return new Promise(function dispatchXhrRequest(resolve, reject) { 12 | const requestData = config.data; 13 | const requestHeaders = config.headers; 14 | 15 | // HTTP basic authentication 16 | if (config.auth) { 17 | const username = config.auth.username || ''; 18 | const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; 19 | requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); 20 | } 21 | 22 | const fullPath = buildFullPath(config.baseURL, config.url); 23 | const request = new Request(buildURL(fullPath, config.params, config.paramsSerializer)); 24 | 25 | // copy headers in 26 | const headers = new Headers(); 27 | for (const key in requestHeaders) { 28 | if (requestHeaders.hasOwnProperty(key)) { 29 | headers.append(key, requestHeaders[key]); 30 | } 31 | } 32 | 33 | const abort = { state: false, schedule: null }; 34 | 35 | if (config.timeout) { 36 | let timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; 37 | if (config.timeoutErrorMessage) { 38 | timeoutErrorMessage = config.timeoutErrorMessage; 39 | } 40 | 41 | abort.schedule = setTimeout(function popTimeout() { 42 | abort.state = true; 43 | reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', config, null)); 44 | }, config.timeout); 45 | } 46 | 47 | const fetcher = fetch(request, { 48 | method: config.method.toUpperCase(), 49 | headers: headers, 50 | body: requestData, 51 | redirect: config.maxRedirects ? 'follow' : 'manual', 52 | }); 53 | 54 | fetcher.then( 55 | function fetchFollowup(response) { 56 | if (abort.state) { 57 | return; 58 | } 59 | if (abort.schedule) { 60 | clearTimeout(abort.schedule); 61 | } 62 | 63 | // Prepare the response 64 | const responseHeaders = response.headers; 65 | let responseData = null; 66 | switch (config.responseType) { 67 | case 'text': 68 | responseData = response.text(); 69 | break; 70 | case 'json': 71 | responseData = response.json(); 72 | break; 73 | case 'blob': 74 | responseData = response.blob(); 75 | break; 76 | default: 77 | responseData = response.text(); 78 | break; 79 | } 80 | 81 | // consume response 82 | if (!responseData) { 83 | reject(createError('Failed to resolve response stream.', config, 'STREAM_FAILED', request, response)); 84 | } else { 85 | responseData.then( 86 | function handleResponseData(data) { 87 | const axiosResponse = { 88 | data: data, 89 | status: response.status, 90 | statusText: response.statusText, 91 | headers: responseHeaders, 92 | config: config, 93 | request: request, 94 | requestHeaders: requestHeaders, 95 | }; 96 | 97 | // we're good to go 98 | settle(resolve, reject, axiosResponse); 99 | }, 100 | function handleDataError(dataErr) { 101 | reject(dataErr || createError('Stream decode error', config, response.statusText, request, response)); 102 | } 103 | ); 104 | } 105 | }, 106 | function handleFetchError(err) { 107 | if (abort.state) { 108 | return; 109 | } 110 | if (abort.schedule) { 111 | clearTimeout(abort.schedule); 112 | } 113 | if (err instanceof Error) { 114 | reject(err); 115 | } else { 116 | reject(createError('Network Error', config, null, request, err)); 117 | } 118 | } 119 | ); 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /lib/utils/logger.js: -------------------------------------------------------------------------------- 1 | module.exports = console; 2 | -------------------------------------------------------------------------------- /lib/utils/mime.js: -------------------------------------------------------------------------------- 1 | const m = { 2 | 'application/andrew-inset': ['ez'], 3 | 'application/applixware': ['aw'], 4 | 'application/atom+xml': ['atom'], 5 | 'application/atomcat+xml': ['atomcat'], 6 | 'application/atomdeleted+xml': ['atomdeleted'], 7 | 'application/atomsvc+xml': ['atomsvc'], 8 | 'application/atsc-dwd+xml': ['dwd'], 9 | 'application/atsc-held+xml': ['held'], 10 | 'application/atsc-rsat+xml': ['rsat'], 11 | 'application/bdoc': ['bdoc'], 12 | 'application/calendar+xml': ['xcs'], 13 | 'application/ccxml+xml': ['ccxml'], 14 | 'application/cdfx+xml': ['cdfx'], 15 | 'application/cdmi-capability': ['cdmia'], 16 | 'application/cdmi-container': ['cdmic'], 17 | 'application/cdmi-domain': ['cdmid'], 18 | 'application/cdmi-object': ['cdmio'], 19 | 'application/cdmi-queue': ['cdmiq'], 20 | 'application/cu-seeme': ['cu'], 21 | 'application/dash+xml': ['mpd'], 22 | 'application/davmount+xml': ['davmount'], 23 | 'application/docbook+xml': ['dbk'], 24 | 'application/dssc+der': ['dssc'], 25 | 'application/dssc+xml': ['xdssc'], 26 | 'application/ecmascript': ['ecma', 'es'], 27 | 'application/emma+xml': ['emma'], 28 | 'application/emotionml+xml': ['emotionml'], 29 | 'application/epub+zip': ['epub'], 30 | 'application/exi': ['exi'], 31 | 'application/fdt+xml': ['fdt'], 32 | 'application/font-tdpfr': ['pfr'], 33 | 'application/geo+json': ['geojson'], 34 | 'application/gml+xml': ['gml'], 35 | 'application/gpx+xml': ['gpx'], 36 | 'application/gxf': ['gxf'], 37 | 'application/gzip': ['gz'], 38 | 'application/hjson': ['hjson'], 39 | 'application/hyperstudio': ['stk'], 40 | 'application/inkml+xml': ['ink', 'inkml'], 41 | 'application/ipfix': ['ipfix'], 42 | 'application/its+xml': ['its'], 43 | 'application/java-archive': ['jar', 'war', 'ear'], 44 | 'application/java-serialized-object': ['ser'], 45 | 'application/java-vm': ['class'], 46 | 'application/javascript': ['js', 'mjs'], 47 | 'application/json': ['json', 'map'], 48 | 'application/json5': ['json5'], 49 | 'application/jsonml+json': ['jsonml'], 50 | 'application/ld+json': ['jsonld'], 51 | 'application/lgr+xml': ['lgr'], 52 | 'application/lost+xml': ['lostxml'], 53 | 'application/mac-binhex40': ['hqx'], 54 | 'application/mac-compactpro': ['cpt'], 55 | 'application/mads+xml': ['mads'], 56 | 'application/manifest+json': ['webmanifest'], 57 | 'application/marc': ['mrc'], 58 | 'application/marcxml+xml': ['mrcx'], 59 | 'application/mathematica': ['ma', 'nb', 'mb'], 60 | 'application/mathml+xml': ['mathml'], 61 | 'application/mbox': ['mbox'], 62 | 'application/mediaservercontrol+xml': ['mscml'], 63 | 'application/metalink+xml': ['metalink'], 64 | 'application/metalink4+xml': ['meta4'], 65 | 'application/mets+xml': ['mets'], 66 | 'application/mmt-aei+xml': ['maei'], 67 | 'application/mmt-usd+xml': ['musd'], 68 | 'application/mods+xml': ['mods'], 69 | 'application/mp21': ['m21', 'mp21'], 70 | 'application/mp4': ['mp4s', 'm4p'], 71 | 'application/mrb-consumer+xml': ['*xdf'], 72 | 'application/mrb-publish+xml': ['*xdf'], 73 | 'application/msword': ['doc', 'dot'], 74 | 'application/mxf': ['mxf'], 75 | 'application/n-quads': ['nq'], 76 | 'application/n-triples': ['nt'], 77 | 'application/node': ['cjs'], 78 | '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'], 79 | 'application/oda': ['oda'], 80 | 'application/oebps-package+xml': ['opf'], 81 | 'application/ogg': ['ogx'], 82 | 'application/omdoc+xml': ['omdoc'], 83 | 'application/onenote': ['onetoc', 'onetoc2', 'onetmp', 'onepkg'], 84 | 'application/oxps': ['oxps'], 85 | 'application/p2p-overlay+xml': ['relo'], 86 | 'application/patch-ops-error+xml': ['*xer'], 87 | 'application/pdf': ['pdf'], 88 | 'application/pgp-encrypted': ['pgp'], 89 | 'application/pgp-signature': ['asc', 'sig'], 90 | 'application/pics-rules': ['prf'], 91 | 'application/pkcs10': ['p10'], 92 | 'application/pkcs7-mime': ['p7m', 'p7c'], 93 | 'application/pkcs7-signature': ['p7s'], 94 | 'application/pkcs8': ['p8'], 95 | 'application/pkix-attr-cert': ['ac'], 96 | 'application/pkix-cert': ['cer'], 97 | 'application/pkix-crl': ['crl'], 98 | 'application/pkix-pkipath': ['pkipath'], 99 | 'application/pkixcmp': ['pki'], 100 | 'application/pls+xml': ['pls'], 101 | 'application/postscript': ['ai', 'eps', 'ps'], 102 | 'application/provenance+xml': ['provx'], 103 | 'application/pskc+xml': ['pskcxml'], 104 | 'application/raml+yaml': ['raml'], 105 | 'application/rdf+xml': ['rdf', 'owl'], 106 | 'application/reginfo+xml': ['rif'], 107 | 'application/relax-ng-compact-syntax': ['rnc'], 108 | 'application/resource-lists+xml': ['rl'], 109 | 'application/resource-lists-diff+xml': ['rld'], 110 | 'application/rls-services+xml': ['rs'], 111 | 'application/route-apd+xml': ['rapd'], 112 | 'application/route-s-tsid+xml': ['sls'], 113 | 'application/route-usd+xml': ['rusd'], 114 | 'application/rpki-ghostbusters': ['gbr'], 115 | 'application/rpki-manifest': ['mft'], 116 | 'application/rpki-roa': ['roa'], 117 | 'application/rsd+xml': ['rsd'], 118 | 'application/rss+xml': ['rss'], 119 | 'application/rtf': ['rtf'], 120 | 'application/sbml+xml': ['sbml'], 121 | 'application/scvp-cv-request': ['scq'], 122 | 'application/scvp-cv-response': ['scs'], 123 | 'application/scvp-vp-request': ['spq'], 124 | 'application/scvp-vp-response': ['spp'], 125 | 'application/sdp': ['sdp'], 126 | 'application/senml+xml': ['senmlx'], 127 | 'application/sensml+xml': ['sensmlx'], 128 | 'application/set-payment-initiation': ['setpay'], 129 | 'application/set-registration-initiation': ['setreg'], 130 | 'application/shf+xml': ['shf'], 131 | 'application/sieve': ['siv', 'sieve'], 132 | 'application/smil+xml': ['smi', 'smil'], 133 | 'application/sparql-query': ['rq'], 134 | 'application/sparql-results+xml': ['srx'], 135 | 'application/srgs': ['gram'], 136 | 'application/srgs+xml': ['grxml'], 137 | 'application/sru+xml': ['sru'], 138 | 'application/ssdl+xml': ['ssdl'], 139 | 'application/ssml+xml': ['ssml'], 140 | 'application/swid+xml': ['swidtag'], 141 | 'application/tei+xml': ['tei', 'teicorpus'], 142 | 'application/thraud+xml': ['tfi'], 143 | 'application/timestamped-data': ['tsd'], 144 | 'application/toml': ['toml'], 145 | 'application/ttml+xml': ['ttml'], 146 | 'application/urc-ressheet+xml': ['rsheet'], 147 | 'application/voicexml+xml': ['vxml'], 148 | 'application/wasm': ['wasm'], 149 | 'application/widget': ['wgt'], 150 | 'application/winhlp': ['hlp'], 151 | 'application/wsdl+xml': ['wsdl'], 152 | 'application/wspolicy+xml': ['wspolicy'], 153 | 'application/xaml+xml': ['xaml'], 154 | 'application/xcap-att+xml': ['xav'], 155 | 'application/xcap-caps+xml': ['xca'], 156 | 'application/xcap-diff+xml': ['xdf'], 157 | 'application/xcap-el+xml': ['xel'], 158 | 'application/xcap-error+xml': ['xer'], 159 | 'application/xcap-ns+xml': ['xns'], 160 | 'application/xenc+xml': ['xenc'], 161 | 'application/xhtml+xml': ['xhtml', 'xht'], 162 | 'application/xliff+xml': ['xlf'], 163 | 'application/xml': ['xml', 'xsl', 'xsd', 'rng'], 164 | 'application/xml-dtd': ['dtd'], 165 | 'application/xop+xml': ['xop'], 166 | 'application/xproc+xml': ['xpl'], 167 | 'application/xslt+xml': ['xslt'], 168 | 'application/xspf+xml': ['xspf'], 169 | 'application/xv+xml': ['mxml', 'xhvml', 'xvml', 'xvm'], 170 | 'application/yang': ['yang'], 171 | 'application/yin+xml': ['yin'], 172 | 'application/zip': ['zip'], 173 | 'audio/3gpp': ['*3gpp'], 174 | 'audio/adpcm': ['adp'], 175 | 'audio/basic': ['au', 'snd'], 176 | 'audio/midi': ['mid', 'midi', 'kar', 'rmi'], 177 | 'audio/mobile-xmf': ['mxmf'], 178 | 'audio/mp3': ['*mp3'], 179 | 'audio/mp4': ['m4a', 'mp4a'], 180 | 'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'], 181 | 'audio/ogg': ['oga', 'ogg', 'spx'], 182 | 'audio/s3m': ['s3m'], 183 | 'audio/silk': ['sil'], 184 | 'audio/wav': ['wav'], 185 | 'audio/wave': ['*wav'], 186 | 'audio/webm': ['weba'], 187 | 'audio/xm': ['xm'], 188 | 'font/collection': ['ttc'], 189 | 'font/otf': ['otf'], 190 | 'font/ttf': ['ttf'], 191 | 'font/woff': ['woff'], 192 | 'font/woff2': ['woff2'], 193 | 'image/aces': ['exr'], 194 | 'image/apng': ['apng'], 195 | 'image/bmp': ['bmp'], 196 | 'image/cgm': ['cgm'], 197 | 'image/dicom-rle': ['drle'], 198 | 'image/emf': ['emf'], 199 | 'image/fits': ['fits'], 200 | 'image/g3fax': ['g3'], 201 | 'image/gif': ['gif'], 202 | 'image/heic': ['heic'], 203 | 'image/heic-sequence': ['heics'], 204 | 'image/heif': ['heif'], 205 | 'image/heif-sequence': ['heifs'], 206 | 'image/hej2k': ['hej2'], 207 | 'image/hsj2': ['hsj2'], 208 | 'image/ief': ['ief'], 209 | 'image/jls': ['jls'], 210 | 'image/jp2': ['jp2', 'jpg2'], 211 | 'image/jpeg': ['jpeg', 'jpg', 'jpe'], 212 | 'image/jph': ['jph'], 213 | 'image/jphc': ['jhc'], 214 | 'image/jpm': ['jpm'], 215 | 'image/jpx': ['jpx', 'jpf'], 216 | 'image/jxr': ['jxr'], 217 | 'image/jxra': ['jxra'], 218 | 'image/jxrs': ['jxrs'], 219 | 'image/jxs': ['jxs'], 220 | 'image/jxsc': ['jxsc'], 221 | 'image/jxsi': ['jxsi'], 222 | 'image/jxss': ['jxss'], 223 | 'image/ktx': ['ktx'], 224 | 'image/png': ['png'], 225 | 'image/sgi': ['sgi'], 226 | 'image/svg+xml': ['svg', 'svgz'], 227 | 'image/t38': ['t38'], 228 | 'image/tiff': ['tif', 'tiff'], 229 | 'image/tiff-fx': ['tfx'], 230 | 'image/webp': ['webp'], 231 | 'image/wmf': ['wmf'], 232 | 'message/disposition-notification': ['disposition-notification'], 233 | 'message/global': ['u8msg'], 234 | 'message/global-delivery-status': ['u8dsn'], 235 | 'message/global-disposition-notification': ['u8mdn'], 236 | 'message/global-headers': ['u8hdr'], 237 | 'message/rfc822': ['eml', 'mime'], 238 | 'model/3mf': ['3mf'], 239 | 'model/gltf+json': ['gltf'], 240 | 'model/gltf-binary': ['glb'], 241 | 'model/iges': ['igs', 'iges'], 242 | 'model/mesh': ['msh', 'mesh', 'silo'], 243 | 'model/mtl': ['mtl'], 244 | 'model/obj': ['obj'], 245 | 'model/stl': ['stl'], 246 | 'model/vrml': ['wrl', 'vrml'], 247 | 'model/x3d+binary': ['*x3db', 'x3dbz'], 248 | 'model/x3d+fastinfoset': ['x3db'], 249 | 'model/x3d+vrml': ['*x3dv', 'x3dvz'], 250 | 'model/x3d+xml': ['x3d', 'x3dz'], 251 | 'model/x3d-vrml': ['x3dv'], 252 | 'text/cache-manifest': ['appcache', 'manifest'], 253 | 'text/calendar': ['ics', 'ifb'], 254 | 'text/coffeescript': ['coffee', 'litcoffee'], 255 | 'text/css': ['css'], 256 | 'text/csv': ['csv'], 257 | 'text/html': ['html', 'htm', 'shtml'], 258 | 'text/jade': ['jade'], 259 | 'text/jsx': ['jsx'], 260 | 'text/less': ['less'], 261 | 'text/markdown': ['markdown', 'md'], 262 | 'text/mathml': ['mml'], 263 | 'text/mdx': ['mdx'], 264 | 'text/n3': ['n3'], 265 | 'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'], 266 | 'text/richtext': ['rtx'], 267 | 'text/rtf': ['*rtf'], 268 | 'text/sgml': ['sgml', 'sgm'], 269 | 'text/shex': ['shex'], 270 | 'text/slim': ['slim', 'slm'], 271 | 'text/stylus': ['stylus', 'styl'], 272 | 'text/tab-separated-values': ['tsv'], 273 | 'text/troff': ['t', 'tr', 'roff', 'man', 'me', 'ms'], 274 | 'text/turtle': ['ttl'], 275 | 'text/uri-list': ['uri', 'uris', 'urls'], 276 | 'text/vcard': ['vcard'], 277 | 'text/vtt': ['vtt'], 278 | 'text/xml': ['*xml'], 279 | 'text/yaml': ['yaml', 'yml'], 280 | 'video/3gpp': ['3gp', '3gpp'], 281 | 'video/3gpp2': ['3g2'], 282 | 'video/h261': ['h261'], 283 | 'video/h263': ['h263'], 284 | 'video/h264': ['h264'], 285 | 'video/jpeg': ['jpgv'], 286 | 'video/jpm': ['*jpm', 'jpgm'], 287 | 'video/mj2': ['mj2', 'mjp2'], 288 | 'video/mp2t': ['ts'], 289 | 'video/mp4': ['mp4', 'mp4v', 'mpg4'], 290 | 'video/mpeg': ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'], 291 | 'video/ogg': ['ogv'], 292 | 'video/quicktime': ['qt', 'mov'], 293 | 'video/webm': ['webm'], 294 | }; 295 | 296 | const map = {}; 297 | Object.keys(m).forEach((t) => { 298 | m[t].forEach((e) => { 299 | map[e] = t; 300 | }); 301 | }); 302 | 303 | map.mkv = 'video/x-matroska'; 304 | map.flv = 'video/x-flv'; 305 | map.m3u8 = 'application/x-mpegURL'; 306 | 307 | module.exports = map; 308 | -------------------------------------------------------------------------------- /lib/utils/node.js: -------------------------------------------------------------------------------- 1 | exports.query2Obj = function (s = '', o = {}) { 2 | for (const [k, v] of new URLSearchParams(s)) { 3 | o[k] = v; 4 | } 5 | return o; 6 | }; 7 | 8 | exports.cookie2Str = function (n, v, o = {}) { 9 | let s = encodeURIComponent(n) + '=' + encodeURIComponent(v); 10 | const { maxAge, domain, path, expires, httpOnly, secure, sameSite } = o; 11 | s += typeof maxAge === 'number' ? '; Max-Age=' + maxAge : ''; 12 | domain ? (s += '; Domain=' + domain) : ''; 13 | path ? (s += '; Path=' + path) : ''; 14 | expires ? (s += '; Expires=' + expires) : ''; 15 | httpOnly ? (s += '; HttpOnly') : ''; 16 | secure ? (s += '; Secure') : ''; 17 | sameSite ? (s += '; SameSite=' + sameSite) : ''; 18 | return s; 19 | }; 20 | 21 | exports._sha1 = require('./tiny-sha1'); 22 | 23 | exports.path = { 24 | basename: (s) => (s ? s.slice(s.lastIndexOf('/') + 1) : ''), 25 | extname: (s) => (s ? s.slice(s.lastIndexOf('.') + 1) : ''), 26 | }; 27 | 28 | const request = (exports.request = require('./tiny-request')); 29 | request.defaults.timeout = 5000; 30 | 31 | exports.exposeHeadersWhenProxy = function (k) { 32 | if (typeof k !== 'string') { 33 | return false; 34 | } 35 | k = k.toLowerCase(); 36 | if (k.startsWith('content-')) { 37 | return k; 38 | } 39 | return ['accept-ranges', 'date'].includes(k); 40 | }; 41 | 42 | const mime = require('./mime.js'); 43 | exports.mime = { 44 | get: (path) => mime[path.slice(path.lastIndexOf('.') + 1)] || 'application/vnd.op-unknown', 45 | }; 46 | 47 | const NUM_CHARS = {}; 48 | '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('').forEach((v, i) => { 49 | NUM_CHARS[i] = v; 50 | NUM_CHARS[v] = i; 51 | }); 52 | 53 | exports.NumberUtil = { 54 | parse62: (s) => { 55 | let num = 0, 56 | base = 1; 57 | s.split('') 58 | .reverse() 59 | .forEach((c) => { 60 | num += NUM_CHARS[c] * base; 61 | base *= 62; 62 | }); 63 | return num; 64 | }, 65 | to62: (n) => { 66 | const arr = []; 67 | while (n > 0) { 68 | arr.push(NUM_CHARS[n % 62]); 69 | n = Math.floor(n / 62); 70 | } 71 | if (arr.length === 0) { 72 | return '0'; 73 | } 74 | return arr.reverse().join(''); 75 | }, 76 | }; 77 | 78 | const { parseErrorMsg } = require('./error-message'); 79 | 80 | class RTError extends Error { 81 | constructor(status, type, data) { 82 | super(parseErrorMsg(type, data || {})); 83 | this.status = status; 84 | this.type = type; 85 | this.data = data || {}; 86 | this.expose = true; 87 | } 88 | } 89 | 90 | exports.RTError = RTError; 91 | 92 | const logger = require('./logger'); 93 | 94 | class IDHelper { 95 | constructor(root) { 96 | this.root = root; 97 | this.icache = {}; 98 | this.etime = Date.now() + 3600 * 1000; 99 | } 100 | 101 | get isValid() { 102 | return Date.now() < this.etime; 103 | } 104 | 105 | async findChildItem(pid, name) { 106 | return Promise.reject(new RTError(500, 'unsupported method: findChildItem(' + pid + ',' + name + ')')); 107 | } 108 | 109 | async getIDByPath(path = '/') { 110 | return this.getItemByPath(path).then((e) => e.id); 111 | } 112 | 113 | async getItemByPath(path) { 114 | return this._getItemByPath( 115 | path.split('/').filter((e) => e), 116 | { type: 1, id: this.root } 117 | ); 118 | } 119 | 120 | async _getItemByPath(paths, item) { 121 | if (paths.length === 0) { 122 | return item; 123 | } 124 | const pid = item.id; 125 | const cache = this.icache[pid] || {}; 126 | const name = paths.shift(); 127 | if (!cache[name] || cache[name].etime < Date.now()) { 128 | logger.info('pid:' + pid + ' ' + name); 129 | const cItem = await this.findChildItem(pid, name); 130 | if (!cItem) { 131 | throw new RTError(404, 'ItemNotExist'); 132 | } 133 | cItem.etime = Date.now() + 300000; 134 | cache[name] = cItem; 135 | // 保证path中出现的都是文件夹 136 | if (cItem.type !== 1 && paths.length > 0) { 137 | throw new RTError(404, 'ItemNotExist'); 138 | } 139 | } 140 | this.icache[pid] = cache; 141 | return this._getItemByPath(paths, cache[name]); 142 | } 143 | } 144 | 145 | exports.IDHelper = IDHelper; 146 | 147 | exports.P = exports._P = (name, value, desc, level, meta, textarea, star) => { 148 | const r = { name, value, desc, level }; 149 | if (Array.isArray(meta)) { 150 | r.select = meta; 151 | } else { 152 | r.placeholder = meta; 153 | if (textarea) { 154 | r.textarea = true; 155 | } 156 | } 157 | if (star) { 158 | r.star = true; 159 | } 160 | if (meta.hidden) { 161 | r.hidden = true; 162 | } 163 | return r; 164 | }; 165 | 166 | exports.beautifyObject = function beautifyObject(ob) { 167 | if (Array.isArray(ob)) { 168 | return ob.map((e) => beautifyObject(e)); 169 | } 170 | if (typeof ob === 'string' || typeof ob === 'number' || typeof ob === 'boolean') { 171 | return ob; 172 | } 173 | const nob = {}; 174 | Object.keys(ob) 175 | .sort() 176 | .forEach((k) => { 177 | nob[k] = typeof ob[k] === 'object' ? beautifyObject(ob[k]) : ob[k]; 178 | }); 179 | return nob; 180 | }; 181 | 182 | exports.deleteAttributes = function (obj, arr) { 183 | arr.forEach((e) => delete obj[e]); 184 | }; 185 | -------------------------------------------------------------------------------- /lib/utils/simple-router.js: -------------------------------------------------------------------------------- 1 | const SimpleRouter = function () { 2 | this.routers = { 3 | GET: {}, 4 | POST: {}, 5 | DELETE: {}, 6 | prefix: [], 7 | regex: [], 8 | default: () => {}, 9 | }; 10 | }; 11 | SimpleRouter.prototype.add = function (m, p, f) { 12 | if (Array.isArray(m)) { 13 | m.forEach((e) => { 14 | this.routers[e][p] = f; 15 | }); 16 | } 17 | if (typeof m === 'string') { 18 | this.routers[m][p] = f; 19 | } 20 | }; 21 | 22 | SimpleRouter.prototype.get = function (p, f) { 23 | this.routers.GET[p] = f; 24 | }; 25 | SimpleRouter.prototype.post = function (p, f) { 26 | this.routers.POST[p] = f; 27 | }; 28 | SimpleRouter.prototype.delete = function (p, f) { 29 | this.routers.DELETE[p] = f; 30 | }; 31 | SimpleRouter.prototype.setDefault = function (f) { 32 | this.routers.default = f; 33 | }; 34 | SimpleRouter.prototype.regex = function (p, f) { 35 | this.routers.regex.push({ p, f }); 36 | }; 37 | SimpleRouter.prototype.prefix = function (p, f) { 38 | this.routers.prefix.push({ p, f }); 39 | }; 40 | SimpleRouter.prototype.handle = async function (ctx, next, path) { 41 | const m = ctx.request.method; 42 | if (this.routers[m] && this.routers[m][path]) { 43 | return this.routers[m][path](ctx); 44 | } 45 | const item = this.routers.regex.find(({ p }) => p.test(path)); 46 | if (item) { 47 | return item.f(ctx, next, path); 48 | } 49 | 50 | const item1 = this.routers.prefix.find(({ p }) => path.startsWith(p)); 51 | if (item1) { 52 | return item1.f(ctx, next, path.slice(item1.p.length)); 53 | } 54 | return this.routers.default(ctx, next, path); 55 | }; 56 | 57 | module.exports = SimpleRouter; 58 | -------------------------------------------------------------------------------- /lib/utils/tiny-request/browser-fetch.js: -------------------------------------------------------------------------------- 1 | function request({ method, url, headers, body }, config) { 2 | return new Promise((resolve, reject) => { 3 | const abort = { state: false, schedule: null }; 4 | 5 | if (config.timeout) { 6 | abort.schedule = setTimeout(() => { 7 | abort.state = true; 8 | reject(new Error('timeout of ' + config.timeout + 'ms exceeded')); 9 | }, config.timeout); 10 | } 11 | fetch(url, { 12 | method, 13 | headers: new Headers(headers), 14 | body, 15 | redirect: 'manual', 16 | }) 17 | .then((res) => { 18 | if (abort.state) { 19 | return; 20 | } 21 | if (abort.schedule) { 22 | clearTimeout(abort.schedule); 23 | } 24 | 25 | const h = {}; 26 | for (const [k, v] of res.headers) { 27 | h[k] = v; 28 | } 29 | if (h['set-cookie']) { 30 | // set-cookie is special 31 | h['set-cookie'] = h['set-cookie'].split(/(? e.trim()); 32 | } 33 | 34 | const response = { 35 | status: res.status, 36 | headers: h, 37 | data: '', 38 | }; 39 | 40 | if (config.responseType === 'stream') { 41 | response.data = res.body; 42 | return resolve(response); 43 | } 44 | 45 | if ((response.status >= 300 && response.status < 400) || response.status === 204 || method === 'HEAD') { 46 | return resolve(response); 47 | } 48 | 49 | // just delete 50 | if (['gzip', 'compress', 'deflate'].includes(response.headers['content-encoding'])) { 51 | delete response.headers['content-encoding']; 52 | } 53 | 54 | let responseData = res; 55 | switch (config.responseType) { 56 | case 'arraybuffer': 57 | responseData = responseData.arrayBuffer(); 58 | break; 59 | case 'blob': 60 | responseData = responseData.blob(); 61 | break; 62 | default: 63 | responseData = responseData.text(); 64 | } 65 | 66 | // consume response 67 | if (!responseData) { 68 | reject(new Error('Failed to resolve response stream.')); 69 | } else { 70 | responseData.then( 71 | (data) => { 72 | response.data = data; 73 | resolve(response); 74 | }, 75 | (dataErr) => { 76 | reject(dataErr || new Error('Stream decode error')); 77 | } 78 | ); 79 | } 80 | }) 81 | .catch((err) => { 82 | if (abort.state) { 83 | return; 84 | } 85 | if (abort.schedule) { 86 | clearTimeout(abort.schedule); 87 | } 88 | if (err instanceof Error) { 89 | reject(err); 90 | } else { 91 | reject(new Error('Network Error')); 92 | } 93 | }); 94 | }); 95 | } 96 | 97 | module.exports = request; 98 | -------------------------------------------------------------------------------- /lib/utils/tiny-request/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface RequestAdapter { 2 | (config: RequestRequestConfig): Promise; 3 | } 4 | 5 | export interface RequestResponseAdapter { 6 | (res: RequestResponse): Promise; 7 | } 8 | 9 | export interface RequestProxyConfig { 10 | host: string; 11 | port: number; 12 | } 13 | 14 | export type Method = 15 | | 'get' | 'GET' 16 | | 'delete' | 'DELETE' 17 | | 'head' | 'HEAD' 18 | | 'options' | 'OPTIONS' 19 | | 'post' | 'POST' 20 | | 'put' | 'PUT' 21 | | 'patch' | 'PATCH' 22 | | 'purge' | 'PURGE' 23 | | 'link' | 'LINK' 24 | | 'unlink' | 'UNLINK' 25 | 26 | export type ResponseType = 27 | | 'arraybuffer' 28 | | 'blob' 29 | | 'document' 30 | | 'json' 31 | | 'text' 32 | | 'stream' 33 | 34 | export interface RequestRequestConfig { 35 | url?: string; 36 | params?: any; 37 | method?: Method; 38 | baseURL?: string; 39 | headers?: any; 40 | body?:any; 41 | data?: any; 42 | timeout?: number; 43 | adapter?: RequestAdapter; 44 | responseType?: ResponseType; 45 | proxy?: RequestProxyConfig; 46 | onResponse?: RequestResponseAdapter; 47 | } 48 | 49 | export interface RequestRequest { 50 | method: Method; 51 | url: string; 52 | headers: any; 53 | body: string | any; 54 | } 55 | 56 | export interface RequestResponse { 57 | status: number; 58 | headers: any; 59 | data: T; 60 | config?: RequestRequestConfig; 61 | request?: RequestRequest; 62 | } 63 | 64 | export interface RequestInstance { 65 | defaults: RequestRequestConfig; 66 | 67 | request | any>(config: RequestRequestConfig): Promise; 68 | 69 | get | any>(url: string, config?: RequestRequestConfig): Promise; 70 | 71 | delete | any>(url: string, config?: RequestRequestConfig): Promise; 72 | 73 | head | any>(url: string, config?: RequestRequestConfig): Promise; 74 | 75 | options | any>(url: string, config?: RequestRequestConfig): Promise; 76 | 77 | post | any>(url: string, data?: any, config?: RequestRequestConfig): Promise; 78 | 79 | put | any>(url: string, data?: any, config?: RequestRequestConfig): Promise; 80 | 81 | patch | any>(url: string, data?: any, config?: RequestRequestConfig): Promise; 82 | 83 | onResponse(f: RequestResponseAdapter): Promise; 84 | } 85 | 86 | export interface RequestStatic extends RequestInstance { 87 | create(config?: RequestRequestConfig): RequestInstance; 88 | } 89 | 90 | declare const request: RequestStatic; 91 | 92 | export default request; 93 | -------------------------------------------------------------------------------- /lib/utils/tiny-request/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 很純粹的http请求工具 3 | * 4 | * 不支持重定向,需要手动处理 5 | * 6 | * method 需要大写 7 | * url 可以包含中文字符 8 | * headers 字段大写格式 9 | * responseType 支持 json text 10 | * 11 | * 支持默认baseURL, headers 12 | */ 13 | 14 | function setIfUndefined(o, a, v) { 15 | o[a] === undefined ? (o[a] = v) : ''; 16 | } 17 | 18 | const adapters = {}; 19 | adapters.n = require('./node-http'); 20 | adapters.f = require('./browser-fetch'); 21 | 22 | function getDefaultAdapter() { 23 | return adapters[typeof fetch === 'function' ? 'f' : 'n']; 24 | } 25 | 26 | function mergeConfig(config1, config2) { 27 | return Object.assign({}, config1, config2, { headers: Object.assign({}, config1.headers, config2.headers) }); 28 | } 29 | 30 | function buildFullURL(url, baseURL, params) { 31 | const u = new URL(url, baseURL || 'http://example.com'); 32 | if (!['http:', 'https:'].includes(u.protocol)) { 33 | // url中包含了 : 34 | if (url[0] === '/') { 35 | u.href = baseURL; 36 | u.pathname = url; 37 | } else { 38 | u.href = baseURL + url; 39 | } 40 | } 41 | 42 | if (params) { 43 | const _searchParams = u.searchParams; 44 | Object.entries(params).forEach(([k, v]) => _searchParams.set(k, v.toString())); 45 | } 46 | 47 | return u.href; 48 | } 49 | 50 | const defaults = { 51 | baseURL: '', 52 | headers: {}, 53 | responseType: 'json', 54 | adapter: getDefaultAdapter(), 55 | onResponse: (d) => d, 56 | }; 57 | 58 | class Request { 59 | constructor(config) { 60 | this.defaults = config; 61 | } 62 | 63 | async request(config) { 64 | config = mergeConfig(this.defaults, config); 65 | 66 | const { url, method, headers, body, data, baseURL, params } = config; 67 | const req = { method: (method || 'GET').toUpperCase(), headers }; 68 | 69 | req.url = buildFullURL(url, baseURL, params); 70 | 71 | setIfUndefined(headers, 'User-Agent', 'tiny-request/0.0'); 72 | setIfUndefined(headers, 'Accept', 'application/json, text/plain, */*'); 73 | 74 | if (['GET', 'HEAD'].includes(req.method)) { 75 | req.body = null; 76 | } else if (body) { 77 | req.body = body; 78 | } else if (data instanceof URLSearchParams) { 79 | setIfUndefined(headers, 'Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); 80 | req.body = data.toString(); 81 | } else if (data && typeof data === 'object') { 82 | setIfUndefined(headers, 'Content-Type', 'application/json;charset=utf-8'); 83 | req.body = JSON.stringify(data); 84 | } else if (typeof data === 'string') { 85 | req.body = data; 86 | } else { 87 | req.body = ''; 88 | } 89 | 90 | return config 91 | .adapter(req, config) 92 | .then((res) => { 93 | if (res.data && config.responseType === 'json' && typeof res.data === 'string') { 94 | try { 95 | res.data = JSON.parse(res.data); 96 | } catch (e) {} 97 | } 98 | res.request = req; 99 | res.config = config; 100 | return res; 101 | }) 102 | .then(config.onResponse) 103 | .catch((e) => { 104 | e.isHttpError = true; 105 | if (Object.getPrototypeOf(e) === Error.prototype) { 106 | e.message = 'Internal HttpError: ' + e.message; 107 | } 108 | return Promise.reject(e); 109 | }); 110 | } 111 | 112 | async reject() {} 113 | } 114 | 115 | // Provide aliases for supported request methods 116 | ['delete', 'get', 'head', 'options'].forEach((m) => { 117 | Request.prototype[m] = function (url, config = {}) { 118 | config.method = m; 119 | config.url = url; 120 | return this.request(config); 121 | }; 122 | }); 123 | 124 | ['post', 'put', 'patch'].forEach((m) => { 125 | Request.prototype[m] = function (url, data = '', config = {}) { 126 | config.method = m; 127 | config.url = url; 128 | config.data = data; 129 | return this.request(config); 130 | }; 131 | }); 132 | 133 | // Create the default instance to be exported 134 | const request = new Request(defaults); 135 | 136 | request.Request = Request; 137 | 138 | // Factory for creating new instances 139 | request.create = function create(config) { 140 | return new Request(mergeConfig(this.defaults, config)); 141 | }; 142 | 143 | module.exports = request; 144 | 145 | // Allow use of default import syntax in TypeScript 146 | module.exports.default = request; 147 | -------------------------------------------------------------------------------- /lib/utils/tiny-request/node-http.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const zlib = require('zlib'); 4 | 5 | // utf8解码未处理bom 6 | async function request({ method, url, headers, body }, config) { 7 | const u = new URL(url); 8 | const options = { 9 | hostname: u.hostname, 10 | port: u.port, 11 | path: u.pathname + u.search, 12 | method, 13 | headers, 14 | }; 15 | 16 | if (config.proxy) { 17 | options.hostname = config.proxy.host; 18 | options.port = config.proxy.port; 19 | options.path = url; 20 | headers.Host = u.host; 21 | } 22 | 23 | return new Promise((resolve, reject) => { 24 | const transport = u.protocol === 'https:' && !config.proxy ? https : http; 25 | 26 | // Create the request 27 | const req = transport.request(options, function handleResponse(res) { 28 | if (req.aborted) { 29 | return; 30 | } 31 | 32 | const response = { 33 | status: res.statusCode, 34 | headers: res.headers, 35 | data: '', 36 | }; 37 | // forward stream, do nothing! 38 | if (config.responseType === 'stream') { 39 | response.data = res; 40 | resolve(response); 41 | return; 42 | } 43 | 44 | // if redirect or no content or HEAD method, do not need body! 45 | if ((response.status >= 300 && response.status < 400) || response.status === 204 || method === 'HEAD') { 46 | resolve(response); 47 | return; 48 | } 49 | 50 | let stream = res; 51 | 52 | if (['gzip', 'compress', 'deflate'].includes(response.headers['content-encoding'])) { 53 | // add the unzipper to the body stream processing pipeline 54 | stream = stream.pipe(zlib.createUnzip()); 55 | // remove the content-encoding in order to not confuse downstream operations 56 | delete response.headers['content-encoding']; 57 | } 58 | 59 | const responseBuffer = []; 60 | stream.on('data', function handleStreamData(chunk) { 61 | responseBuffer.push(chunk); 62 | }); 63 | 64 | stream.on('error', function handleStreamError(err) { 65 | if (req.aborted) { 66 | return; 67 | } 68 | reject(err); 69 | }); 70 | 71 | stream.on('end', function handleStreamEnd() { 72 | let responseData = Buffer.concat(responseBuffer); 73 | if (config.responseType !== 'arraybuffer') { 74 | responseData = responseData.toString('utf8'); 75 | } 76 | response.data = responseData; 77 | resolve(response); 78 | }); 79 | }); 80 | 81 | // Handle errors 82 | req.on('error', function handleRequestError(err) { 83 | if (req.aborted) { 84 | return; 85 | } 86 | reject(err); 87 | }); 88 | 89 | // Handle request timeout 90 | if (config.timeout) { 91 | // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system. 92 | // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET. 93 | // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. 94 | // And then these socket which be hang up will devoring CPU little by little. 95 | // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. 96 | req.setTimeout(config.timeout, function handleRequestTimeout() { 97 | req.abort(); 98 | reject(new Error('timeout of ' + config.timeout + 'ms exceeded')); 99 | }); 100 | } 101 | 102 | // Send the request 103 | req.end(body); 104 | }); 105 | } 106 | 107 | module.exports = request; 108 | -------------------------------------------------------------------------------- /lib/utils/tiny-sha1.js: -------------------------------------------------------------------------------- 1 | // https://github.com/jbt/tiny-hashes/blob/master/sha1/sha1.js 2 | function sha1(b) { 3 | var i, 4 | W = [], 5 | A, 6 | B, 7 | C, 8 | D, 9 | h = [(A = 0x67452301), (B = 0xefcdab89), ~A, ~B, 0xc3d2e1f0], 10 | words = [], 11 | s = unescape(encodeURI(b)) + '\x80', 12 | j = s.length; 13 | 14 | // See "Length bits" in notes 15 | words[(b = (--j / 4 + 2) | 15)] = j * 8; 16 | 17 | for (; ~j; ) { 18 | // j !== -1 19 | words[j >> 2] |= s.charCodeAt(j) << (8 * ~j--); 20 | // words[j >> 2] |= s.charCodeAt(j) << 24 - 8 * j--; 21 | } 22 | 23 | for (i = j = 0; i < b; i += 16) { 24 | A = h; 25 | 26 | for ( 27 | ; 28 | j < 80; 29 | A = [ 30 | A[4] + 31 | (W[j] = j < 16 ? ~~words[i + j] : (s * 2) | (s < 0)) + // s << 1 | s >>> 31 32 | 1518500249 + 33 | [(B & C) | (~B & D), (s = (B ^ C ^ D) + 341275144), ((B & C) | (B & D) | (C & D)) + 882459459, s + 1535694389][/* 0 | (j++ / 20)*/ (j++ / 5) >> 2] + 34 | (((s = A[0]) << 5) | (s >>> 27)), 35 | s, 36 | (B << 30) | (B >>> 2), 37 | C, 38 | D, 39 | ] 40 | ) { 41 | s = W[j - 3] ^ W[j - 8] ^ W[j - 14] ^ W[j - 16]; 42 | B = A[1]; 43 | C = A[2]; 44 | D = A[3]; 45 | } 46 | 47 | // See "Integer safety" in notes 48 | for (j = 5; j; ) { 49 | h[--j] += A[j]; 50 | } 51 | 52 | // j === 0 53 | } 54 | 55 | for (s = ''; j < 40; ) { 56 | // s += ((h[j >> 3] >> 4 * ~j++) & 15).toString(16); 57 | s += ((h[j >> 3] >> ((7 - j++) * 4)) & 15).toString(16); 58 | // s += ((h[j >> 3] >> -4 * ++j) & 15).toString(16); 59 | } 60 | 61 | return s; 62 | } 63 | 64 | module.exports = sha1; 65 | -------------------------------------------------------------------------------- /lib/utils/view-helper.js: -------------------------------------------------------------------------------- 1 | const op = require('../core/op'); 2 | 3 | class V { 4 | constructor(ctx) { 5 | this.ctx = ctx; 6 | this.request = ctx.request; 7 | this.response = ctx.response; 8 | this.site = op.config.site; 9 | } 10 | 11 | get navs() { 12 | const path = this.request.path; 13 | const q = this.request.query; 14 | const arr = [{ name: 'root' }]; 15 | path.split('/') 16 | .filter((e) => e) 17 | .forEach((e) => { 18 | arr.push({ name: e }); 19 | }); 20 | if (path.endsWith('/')) { 21 | let p = './'; 22 | for (let i = arr.length - 1; i >= 0; i--) { 23 | arr[i].href = p; 24 | p += '../'; 25 | } 26 | if (q.id) { 27 | arr[arr.length - 1].href += '?id=' + encodeURIComponent(q.id); 28 | } 29 | } else { 30 | arr[arr.length - 1].href = (q.preview !== undefined ? '?preview' : '?') + this.appendReqQueryID; 31 | let p = './'; 32 | for (let i = arr.length - 2; i >= 0; i--) { 33 | arr[i].href = p; 34 | p += '../'; 35 | } 36 | } 37 | return arr; 38 | } 39 | 40 | get list() { 41 | return this.response.data.list; 42 | } 43 | 44 | get hasPrev() { 45 | return this.response.data.prevToken; 46 | } 47 | 48 | get prevHref() { 49 | return '?page=' + encodeURIComponent(this.response.data.prevToken) + this.appendReqQueryID; 50 | } 51 | 52 | get hasParent() { 53 | return this.request.path !== '/'; 54 | } 55 | 56 | get hasNext() { 57 | return this.response.data.nextToken; 58 | } 59 | 60 | get nextHref() { 61 | return '?page=' + encodeURIComponent(this.response.data.nextToken) + this.appendReqQueryID; 62 | } 63 | 64 | get appendReqQueryID() { 65 | const id = this.request.query.id; 66 | return id ? '&id=' + encodeURIComponent(id) : ''; 67 | } 68 | 69 | get isEmpty() { 70 | return this.response.data.list.length === 0; 71 | } 72 | 73 | previewHref(e, p = true) { 74 | if (e.type === 0) { 75 | return `${e.name}${e.id ? '?id=' + encodeURIComponent(e.id) : ''}${p ? (e.id ? '&preview' : '?preview') : ''}`; 76 | } else { 77 | return `${e.name}/${e.id ? '?id=' + encodeURIComponent(e.id) : ''}`; 78 | } 79 | } 80 | 81 | get file() { 82 | return this.response.data.file; 83 | } 84 | 85 | get previewType() { 86 | const f = this.file; 87 | const m = f.mime; 88 | if (m.startsWith('image/')) { 89 | return 'image'; 90 | } 91 | if (m.startsWith('video/')) { 92 | return 'video'; 93 | } 94 | if (m.startsWith('audio/')) { 95 | return 'audio'; 96 | } 97 | if (['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'mpp', 'rtf', 'vsd', 'vsdx'].includes(f.name.slice(f.name.lastIndexOf('.') + 1))) { 98 | return 'office'; 99 | } 100 | if (m.endsWith('pdf')) { 101 | return 'pdf'; 102 | } 103 | if (f.size < 16 * 1024) { 104 | return 'text'; 105 | } 106 | if (m.startsWith('text')) { 107 | return 'bigText'; 108 | } 109 | return ''; 110 | } 111 | 112 | // @warning 考虑放弃proxy功能 113 | get downloadUrl() { 114 | return (this.request.cookies.PROXY_DOWN || '') + this.response.data.file.url; 115 | } 116 | 117 | get hasPassword() { 118 | return this.response.data.error === 'Unauthorized'; 119 | } 120 | 121 | get passwordHint() { 122 | const { type, field } = this.response.data.data; 123 | return field + ' ' + type; 124 | } 125 | 126 | get jsonData() { 127 | return JSON.stringify(this.response.data, null, 2); 128 | } 129 | 130 | get readme() { 131 | return ((this.ctx.$node || {}).$config || {}).readme || op.config.site.readme; 132 | } 133 | 134 | get readmeUrl() { 135 | return this.response.isList && this.response.data.list.find((e) => e.name === 'README.md') ? 'README.md' : ''; 136 | } 137 | 138 | get cacheTime() { 139 | return this.response.data.cached; 140 | } 141 | 142 | get refreshHref() { 143 | const q = this.request.query; 144 | return '?refresh' + (q.preview === undefined ? '' : '&preview') + this.appendReqQueryID + (q.page ? '&page=' + encodeURIComponent(q.page) : ''); 145 | } 146 | 147 | encodeURIComponent(u) { 148 | return encodeURIComponent(u); 149 | } 150 | } 151 | 152 | module.exports = V; 153 | -------------------------------------------------------------------------------- /lib/views/art-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*! art-template@runtime | https://github.com/aui/art-template */ 4 | 5 | var globalThis = typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {}; 6 | 7 | var runtime = Object.create(globalThis); 8 | var ESCAPE_REG = /["&'<>]/; 9 | 10 | /** 11 | * 编码模板输出的内容 12 | * @param {any} content 13 | * @return {string} 14 | */ 15 | runtime.$escape = function (content) { 16 | return xmlEscape(toString(content)); 17 | }; 18 | 19 | /** 20 | * 迭代器,支持数组与对象 21 | * @param {array|Object} data 22 | * @param {function} callback 23 | */ 24 | runtime.$each = function (data, callback) { 25 | if (Array.isArray(data)) { 26 | for (var i = 0, len = data.length; i < len; i++) { 27 | callback(data[i], i); 28 | } 29 | } else { 30 | for (var _i in data) { 31 | callback(data[_i], _i); 32 | } 33 | } 34 | }; 35 | 36 | // 将目标转成字符 37 | function toString(value) { 38 | if (typeof value !== 'string') { 39 | if (value === undefined || value === null) { 40 | value = ''; 41 | } else if (typeof value === 'function') { 42 | value = toString(value.call(value)); 43 | } else { 44 | value = JSON.stringify(value); 45 | } 46 | } 47 | 48 | return value; 49 | } 50 | 51 | // 编码 HTML 内容 52 | function xmlEscape(content) { 53 | var html = '' + content; 54 | var regexResult = ESCAPE_REG.exec(html); 55 | if (!regexResult) { 56 | return content; 57 | } 58 | 59 | var result = ''; 60 | var i = void 0, 61 | lastIndex = void 0, 62 | char = void 0; 63 | for (i = regexResult.index, lastIndex = 0; i < html.length; i++) { 64 | switch (html.charCodeAt(i)) { 65 | case 34: 66 | char = '"'; 67 | break; 68 | case 38: 69 | char = '&'; 70 | break; 71 | case 39: 72 | char = '''; 73 | break; 74 | case 60: 75 | char = '<'; 76 | break; 77 | case 62: 78 | char = '>'; 79 | break; 80 | default: 81 | continue; 82 | } 83 | 84 | if (lastIndex !== i) { 85 | result += html.substring(lastIndex, i); 86 | } 87 | 88 | lastIndex = i + 1; 89 | result += char; 90 | } 91 | 92 | if (lastIndex !== i) { 93 | return result + html.substring(lastIndex, i); 94 | } else { 95 | return result; 96 | } 97 | } 98 | 99 | module.exports = runtime; -------------------------------------------------------------------------------- /lib/views/art/simple.art: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $V.site.name }} 8 | 9 | 10 | 11 |
12 |
13 | {{ set navs=$V.navs}} 14 | {{ each navs}} 15 | {{ if $index===0 }} 16 | Home 17 | {{else}} 18 | / 19 | {{ $value.name }} 20 | {{ /if }} 21 | {{ /each }} 22 |
23 | {{ if response.isList}} 24 | {{ each $V.list }} 25 |
26 | {{ $value.name }} 27 |
28 | {{ /each }} 29 | {{ if $V.hasNext }} 30 |
31 | Next... 32 |
33 | {{ /if }} 34 | {{ else }} 35 |
36 |
{{ $V.jsonData }}
37 |
38 | {{ /if }} 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/views/art/w.w.art: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ $V.site.name }} 13 | 31 | 32 | 33 | 41 |
42 | 50 | {{ if response.isList }} 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {{ if $V.hasPrev }} 62 | 63 | 64 | 65 | 66 | 67 | {{ else if $V.hasParent }} 68 | 69 | 70 | 71 | 72 | 73 | {{ /if }} 74 | 75 | {{ each $V.list }} 76 | 77 | 80 | 81 | 82 | 83 | {{ /each }} 84 | 85 | {{ if $V.hasNext }} 86 | 87 | 88 | 89 | 90 | 91 | {{ /if }} 92 | 93 |
NameTimeSize
👆Previous...
👈..
{{ ($value.type===0?'':'📁') + $value.name}}{{ $value.time }}{{ $value.size }}
👇Next...
94 | 128 | {{ if $V.isEmpty }} 129 |

Empty Folder!

130 | {{ /if }} 131 |
132 | 133 | {{ else if response.isFile }} 134 | 135 | {{ set type=$V.previewType }} 136 | {{ set url=$V.downloadUrl }} 137 | {{ set oUrl=$V.previewHref($V.file,false)}} 138 |
139 | 140 |
141 | 144 | 下 载 145 |
146 |
147 | 148 |
149 | {{ if type === 'image' }} 150 | 图片加载失败 151 | {{ else if type === 'video' || $V.file.name.endsWith('.m3u8') }} 152 |
153 | 154 | 155 | 156 | 163 | {{ else if type === 'audio' }} 164 | 165 | {{ else if type === 'office' }} 166 | 175 | {{ else if type==='pdf' }} 176 |
177 | 178 | 179 | {{ else if type==='text' }} 180 |
loading...
181 | 182 | 183 | 191 | {{ else if type==='bigText'}} 192 |

该文本文件太大, 不支持预览 :-(

193 | {{ else }} 194 |

此格式({{ $V.file.mime }})不支持预览 :-(

195 | {{ /if }} 196 |
197 | 198 | 204 | 205 | {{ else }} 206 | 207 | {{ if $V.hasPassword }} 208 |
209 |
210 |
211 | 214 |
215 | 216 |
217 |
218 | {{ /if }} 219 |
220 |
{{ response.message }}
221 |
{{ $V.jsonData }}
222 |
223 | {{ /if }} 224 | 225 |
226 |
README
227 |
{{ $V.readme }}
228 | 229 | 230 | {{ if $V.readmeUrl }} 231 | 234 | {{ /if }} 235 |
236 | 240 | {{@ $V.site.html }} 241 |
242 | 243 | -------------------------------------------------------------------------------- /lib/views/art2js.js: -------------------------------------------------------------------------------- 1 | //https://github.com/kangax/html-minifier/issues/1076 2 | require('art-template/node_modules/html-minifier').minify = require('html-minifier-terser').minify; 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const ART_PATH = path.join(__dirname, 'art/'); 7 | const JS_PATH = path.join(__dirname, 'js/'); 8 | const art = require('art-template'); 9 | const artOpt = {debug: false}; 10 | module.exports = function (flag) { 11 | fs.readdirSync(ART_PATH).forEach(e => { 12 | const src = ART_PATH + e; 13 | const des = JS_PATH + e + '.js'; 14 | if (!fs.existsSync(des) || fs.statSync(src).mtimeMs > fs.statSync(des).mtimeMs) { 15 | console.log("update: " + e); 16 | const r = art.compile(fs.readFileSync(src, 'utf-8'), artOpt); 17 | fs.writeFileSync(des, `const $imports = require('../art-runtime');module.exports={name:"${e}"};module.exports.render=` + r.toString(), {encoding: 'utf-8'}); 18 | console.log("update success: " + e); 19 | require(des).render = r; 20 | } else { 21 | console.log("ignore: " + e); 22 | } 23 | //@important 仅为方便测试使用,release前需要手动运行一次,将数据写入js。 24 | const desJs = require(des); 25 | flag && fs.watchFile(src, stats => { 26 | desJs.render = art.compile(fs.readFileSync(src, 'utf-8'), artOpt); 27 | console.log('update file:' + e + ', ' + stats.mtime.toLocaleString()); 28 | }); 29 | }); 30 | }; -------------------------------------------------------------------------------- /lib/views/js/simple.art.js: -------------------------------------------------------------------------------- 1 | const $imports = require('../art-runtime');module.exports={name:"simple.art"};module.exports.render=function($data){ 2 | 'use strict' 3 | $data=$data||{} 4 | var $$out='',$escape=$imports.$escape,$V=$data.$V,navs=$data.navs,$each=$imports.$each,$value=$data.$value,$index=$data.$index,response=$data.response 5 | $$out+="" 8 | $$out+=$escape($V.site.name) 9 | $$out+="
" 10 | var navs=$V.navs 11 | $$out+=" " 12 | $each(navs,function($value,$index){ 13 | $$out+=" " 14 | if($index===0){ 15 | $$out+=" Home " 18 | }else{ 19 | $$out+=" / " 22 | $$out+=$escape($value.name) 23 | $$out+=" " 24 | } 25 | $$out+=" " 26 | }) 27 | $$out+="
" 28 | if(response.isList){ 29 | $$out+=" " 30 | $each($V.list,function($value,$index){ 31 | $$out+=" " 36 | }) 37 | $$out+=" " 38 | if($V.hasNext){ 39 | $$out+=" " 42 | } 43 | $$out+=" " 44 | }else{ 45 | $$out+="
"
46 | $$out+=$escape($V.jsonData)
47 | $$out+="
" 48 | } 49 | $$out+="
" 50 | return $$out 51 | } -------------------------------------------------------------------------------- /lib/views/js/w.w.art.js: -------------------------------------------------------------------------------- 1 | const $imports = require('../art-runtime');module.exports={name:"w.w.art"};module.exports.render=function($data){ 2 | 'use strict' 3 | $data=$data||{} 4 | var $$out='',$escape=$imports.$escape,$V=$data.$V,navs=$data.navs,$each=$imports.$each,$value=$data.$value,$index=$data.$index,response=$data.response,type=$data.type,url=$data.url,oUrl=$data.oUrl 5 | $$out+="" 8 | $$out+=$escape($V.site.name) 9 | $$out+="
" 24 | if(response.isList){ 25 | $$out+="
" 26 | if($V.hasPrev){ 27 | $$out+=" " 30 | }else if($V.hasParent){ 31 | $$out+=" " 32 | } 33 | $$out+=" " 34 | $each($V.list,function($value,$index){ 35 | $$out+=" " 46 | }) 47 | $$out+=" " 48 | if($V.hasNext){ 49 | $$out+=" " 52 | } 53 | $$out+="
NameTimeSize
👆Previous...
👈..
" 40 | $$out+=$escape(($value.type===0?'':'📁') + $value.name) 41 | $$out+="" 42 | $$out+=$escape($value.time) 43 | $$out+="" 44 | $$out+=$escape($value.size) 45 | $$out+="
👇Next...
" 54 | if($V.isEmpty){ 55 | $$out+="

Empty Folder!

" 56 | } 57 | $$out+="
" 58 | }else if(response.isFile){ 59 | $$out+=" " 60 | var type=$V.previewType 61 | $$out+=" " 62 | var url=$V.downloadUrl 63 | $$out+=" " 64 | var oUrl=$V.previewHref($V.file,false) 65 | $$out+="
下 载
" 68 | if(type === 'image'){ 69 | $$out+=" \"图片加载失败\" " 72 | }else if(type === 'video' || $V.file.name.endsWith('.m3u8')){ 73 | $$out+="
" 76 | }else if(type === 'audio'){ 77 | $$out+=" " 80 | }else if(type === 'office'){ 81 | $$out+=" " 86 | }else if(type==='pdf'){ 87 | $$out+="
" 90 | }else if(type==='text'){ 91 | $$out+="
loading...
" 94 | }else if(type==='bigText'){ 95 | $$out+="

该文本文件太大, 不支持预览 :-(

" 96 | }else{ 97 | $$out+="

此格式(" 98 | $$out+=$escape($V.file.mime) 99 | $$out+=")不支持预览 :-(

" 100 | } 101 | $$out+="
" 102 | }else{ 103 | $$out+=" " 104 | if($V.hasPassword){ 105 | $$out+="
" 108 | } 109 | $$out+="
" 110 | $$out+=$escape(response.message) 111 | $$out+="
"
112 | $$out+=$escape($V.jsonData)
113 | $$out+="
" 114 | } 115 | $$out+="
README
" 118 | $$out+=$escape($V.readme) 119 | $$out+="
" 120 | if($V.readmeUrl){ 121 | $$out+=" " 122 | } 123 | $$out+="
" 128 | $$out+=$V.site.html 129 | $$out+="
" 130 | return $$out 131 | } -------------------------------------------------------------------------------- /lib/views/js/w.w.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | render: () => {} 3 | }; 4 | -------------------------------------------------------------------------------- /ncc/build.js: -------------------------------------------------------------------------------- 1 | //修改文件后 可以通过此脚本重新编译发布 2 | const {readdirSync, writeFileSync} = require('fs'); 3 | const ncc = require('@vercel/ncc'); 4 | 5 | const APP_PATH = require('path').resolve(__dirname, '../lib'); 6 | const DIST_PATH = require('path').resolve(__dirname, '../ncc'); 7 | 8 | readdirSync(APP_PATH + '/starters').forEach(name => { 9 | if (name.startsWith('local')) { 10 | return; 11 | } 12 | ncc(APP_PATH + '/starters/' + name, { 13 | // provide a custom cache path or disable caching 14 | cache: false, 15 | // externals to leave as requires of the build 16 | externals: [], 17 | // directory outside of which never to emit assets 18 | filterAssetBase: APP_PATH, // default 19 | minify: false, // default 20 | sourceMap: false, // default 21 | sourceMapBasePrefix: '../', // default treats sources as output-relative 22 | // when outputting a sourcemap, automatically include 23 | // source-map-support in the output file (increases output by 32kB). 24 | sourceMapRegister: true, // default 25 | watch: false, // default 26 | license: '', // default does not generate a license file 27 | v8cache: false, // default 28 | quiet: false, // default 29 | debugLog: false // default 30 | }).then(({code}) => { 31 | if (name.startsWith('cf')) { 32 | code = 'globalThis.__dirname="";\n' + code; 33 | } 34 | writeFileSync(DIST_PATH + '/ncc_' + name, code); 35 | }); 36 | }); 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "ukuq", 3 | "bugs": { 4 | "url": "https://github.com/ukuq/onepoint/issues" 5 | }, 6 | "dependencies": {}, 7 | "description": "a tiny file index and manage program", 8 | "devDependencies": { 9 | "@vercel/ncc": "^0.28.6", 10 | "art-template": "^4.13.2", 11 | "eslint": "^7.16.0", 12 | "eslint-config-prettier": "^7.1.0", 13 | "eslint-plugin-prettier": "^3.3.0", 14 | "html-minifier-terser": "^5.1.1", 15 | "prettier": "^2.2.1" 16 | }, 17 | "files": [ 18 | "lib" 19 | ], 20 | "homepage": "https://github.com/ukuq/onepoint#readme", 21 | "keywords": [ 22 | "onepoint", 23 | "onedrive", 24 | "google-drive", 25 | "scf", 26 | "serverless" 27 | ], 28 | "license": "MIT", 29 | "main": "lib/app.js", 30 | "name": "onepoint", 31 | "prettier": { 32 | "printWidth": 233, 33 | "singleQuote": true, 34 | "tabWidth": 4, 35 | "trailingComma": "es5" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/ukuq/onepoint.git" 40 | }, 41 | "scripts": { 42 | "__local_start": "node lib/starters/local-test.js", 43 | "__pre_commit": "node tmp/pre-commit.js", 44 | "build:ncc": "node ncc/build.js", 45 | "format": "eslint \"**/*.js\" --fix && prettier \"**/*.{js,json}\" --write", 46 | "format:check": "eslint \"**/*.js\" && prettier \"**/*.{js,json}\" --check", 47 | "start": "node lib/starters/node-http.js" 48 | }, 49 | "version": "2.0.1", 50 | "version2": "210620" 51 | } 52 | -------------------------------------------------------------------------------- /worker/README.md: -------------------------------------------------------------------------------- 1 | ## Cloudflare Workers 部署 2 | 3 | ### 新建 Worker 4 | ![image.png](https://i.loli.net/2021/02/19/5tsTuklZUDWSIix.png) 5 | 6 | ### 粘贴代码 7 | 8 | https://github.com/ukuq/onepoint/blob/master/ncc/ncc_cf-worker.js 9 | 10 | 或者 https://raw.githubusercontent.com/ukuq/onepoint/master/ncc/ncc_cf-worker.js 11 | 12 | 代码较多,保存可能要费点时间,不要心急! 13 | 14 | ![image.png](https://i.loli.net/2021/02/19/92xyFLK5dOrWk4s.png) 15 | 16 | ### 返回上一级,新建KV桶,名字随意 17 | 18 | ![image.png](https://i.loli.net/2021/02/19/Ep2rmbQN9y1TFDI.png) 19 | ![image.png](https://i.loli.net/2021/02/19/WfnypotgmCHuPqh.png) 20 | 21 | ### 绑定KV桶,变量名设置为OPCONFIG 22 | 23 | ![image.png](https://i.loli.net/2021/02/19/W6MOmlRYTi53oQZ.png) 24 | 25 | ### DEMO 26 | 27 | https://onepoint.onesrc.workers.dev/ 28 | 29 | ### DEV 30 | 31 | 如需修改代码,可以 git clone,修改前后使用 ncc 打包 32 | 33 | --------------------------------------------------------------------------------