Repository: reruin/sharelist Branch: master Commit: 22c46c56e798 Files: 226 Total size: 589.4 KB Directory structure: gitextract_oo5ouzu4/ ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── .prettierrc.js ├── .yarnrc ├── LICENSE ├── README.md ├── docs/ │ ├── .nojekyll │ ├── README.md │ ├── _navbar.md │ ├── en/ │ │ ├── README.md │ │ ├── _navbar.md │ │ ├── _sidebar.md │ │ └── installation.md │ ├── index.html │ └── zh-cn/ │ ├── README.md │ ├── _navbar.md │ ├── _sidebar.md │ ├── advance.md │ ├── configuration.md │ ├── dev.md │ └── installation.md ├── package.json ├── packages/ │ ├── sharelist/ │ │ ├── CHANGELOG.md │ │ ├── Dockerfile │ │ ├── LICENSE │ │ ├── README.md │ │ ├── app/ │ │ │ ├── config.js │ │ │ ├── main.js │ │ │ ├── modules/ │ │ │ │ ├── command/ │ │ │ │ │ └── index.js │ │ │ │ ├── core/ │ │ │ │ │ ├── cache.js │ │ │ │ │ ├── config.js │ │ │ │ │ ├── db.js │ │ │ │ │ ├── http.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── plugin.js │ │ │ │ │ ├── task.js │ │ │ │ │ ├── theme.js │ │ │ │ │ ├── upload.js │ │ │ │ │ └── utils.js │ │ │ │ ├── guide/ │ │ │ │ │ ├── driver/ │ │ │ │ │ │ ├── aliyundrive.js │ │ │ │ │ │ ├── baidu.js │ │ │ │ │ │ ├── googledrive.js │ │ │ │ │ │ ├── onedrive.js │ │ │ │ │ │ └── shared.js │ │ │ │ │ └── index.js │ │ │ │ ├── server/ │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── router.js │ │ │ │ │ └── runtime.js │ │ │ │ └── webdav/ │ │ │ │ └── index.js │ │ │ └── shared/ │ │ │ └── send.js │ │ ├── app.js │ │ ├── docker-compose.yml │ │ └── package.json │ ├── sharelist-core/ │ │ ├── .prettierrc.js │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── action.js │ │ │ ├── driver.js │ │ │ ├── index.js │ │ │ ├── rectifier.js │ │ │ ├── request.js │ │ │ └── utils.js │ │ ├── package.json │ │ └── test/ │ │ ├── index.js │ │ └── plugin/ │ │ └── driver.test.js │ ├── sharelist-manage/ │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .prettierrc.js │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── assets/ │ │ │ │ └── style/ │ │ │ │ ├── index.less │ │ │ │ └── var.less │ │ │ ├── components/ │ │ │ │ ├── code-editor/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── icon/ │ │ │ │ │ ├── icon-svg.js │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.ts │ │ │ │ ├── image/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── modal/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── player/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── sider/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── components.d.ts │ │ │ ├── config/ │ │ │ │ ├── api.ts │ │ │ │ └── setting.ts │ │ │ ├── hooks/ │ │ │ │ ├── useApi.ts │ │ │ │ ├── useClipboard.ts │ │ │ │ ├── useConfirm.ts │ │ │ │ ├── useDirective.ts │ │ │ │ ├── useDom.ts │ │ │ │ ├── useHooks.ts │ │ │ │ ├── useLoad.ts │ │ │ │ ├── useLocalStorage.ts │ │ │ │ ├── useRequest.ts │ │ │ │ ├── useScroll.ts │ │ │ │ ├── useSetting.ts │ │ │ │ ├── useStore.ts │ │ │ │ ├── useUrlState.ts │ │ │ │ ├── useWorker.ts │ │ │ │ └── utils.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── router/ │ │ │ │ └── index.ts │ │ │ ├── store/ │ │ │ │ └── index.ts │ │ │ ├── types/ │ │ │ │ ├── IDrive.ts │ │ │ │ ├── shim.d.ts │ │ │ │ └── source.d.ts │ │ │ ├── utils/ │ │ │ │ └── format.ts │ │ │ └── views/ │ │ │ ├── disk/ │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── partial/ │ │ │ │ ├── action/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── auth/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── breadcrumb/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── error/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── meta/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── modifier/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── search/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── task/ │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── upload/ │ │ │ │ │ └── index.tsx │ │ │ │ └── useDisk.ts │ │ │ ├── general/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── home/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── plugin/ │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── partial/ │ │ │ │ └── store/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── signin/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── test/ │ │ │ └── index.vue │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── sharelist-web/ │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .prettierrc.js │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── assets/ │ │ │ │ └── style/ │ │ │ │ ├── index.less │ │ │ │ └── var.less │ │ │ ├── components/ │ │ │ │ ├── icon/ │ │ │ │ │ ├── icon-svg.js │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.ts │ │ │ │ ├── image/ │ │ │ │ │ └── index.tsx │ │ │ │ └── player/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── config/ │ │ │ │ ├── api.ts │ │ │ │ └── setting.ts │ │ │ ├── hooks/ │ │ │ │ ├── useApi.ts │ │ │ │ ├── useDisk.ts │ │ │ │ ├── useHooks.ts │ │ │ │ ├── useLocalStorage.ts │ │ │ │ ├── useScroll.ts │ │ │ │ ├── useSetting.ts │ │ │ │ └── utils.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── router/ │ │ │ │ └── index.ts │ │ │ ├── store/ │ │ │ │ └── index.ts │ │ │ ├── types/ │ │ │ │ ├── IDrive.ts │ │ │ │ ├── IRequest.ts │ │ │ │ ├── shim.d.ts │ │ │ │ └── source.d.ts │ │ │ ├── utils/ │ │ │ │ └── format.ts │ │ │ └── views/ │ │ │ └── home/ │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── partial/ │ │ │ ├── auth/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── breadcrumb/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── error/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── header/ │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── sharelist-webdav/ │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── context.ts │ │ ├── index.ts │ │ ├── operations/ │ │ │ ├── commands.ts │ │ │ ├── copy.ts │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── head.ts │ │ │ ├── lock.ts │ │ │ ├── mkcol.ts │ │ │ ├── move.ts │ │ │ ├── not-implemented.ts │ │ │ ├── options.ts │ │ │ ├── post.ts │ │ │ ├── propfind.ts │ │ │ ├── proppatch.ts │ │ │ ├── put.ts │ │ │ ├── shared.ts │ │ │ └── unlock.ts │ │ └── types.ts │ └── tsconfig.json └── scripts/ ├── build.js ├── changelog.js ├── netinstall.sh └── release.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { parser: 'vue-eslint-parser', parserOptions: { // set script parser parser: '@typescript-eslint/parser', // Specifies the ESLint parser ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features sourceType: 'module', // Allows for the use of imports ecmaFeatures: { jsx: true, // Allows for the parsing of JSX }, validate: [], }, extends: [ 'plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 'plugin:prettier/recommended', ], rules: { // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-var-requires': 'off', 'prettier/prettier': ['off', { endOfLine: 'auto' }], }, } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: "Bug report" description: 问题报告 labels: [pending triage] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: bug-description attributes: label: 问题描述 / Describe the bug validations: required: true - type: dropdown id: version attributes: label: Sharelist 版本 / Sharelist Version description: Sharelist Version options: - v0.1 - next validations: required: true - type: textarea id: reproduction attributes: label: 复现链接 / Reproduction description: | 请提供能复现此问题的链接 Please provide a link to a repo that can reproduce the problem you ran into. validations: required: false - type: textarea id: logs attributes: label: 日志 / Logs description: | 请复制错误日志,或者截图 Please copy paste the log text. render: shell ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Questions & Discussions url: https://github.com/reruin/sharelist/discussions about: Use GitHub discussions for message-board style questions and discussions. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: "Feature request" description: 功能需求 labels: ["enhancement: pending triage"] body: - type: textarea id: feature-description attributes: label: 需求描述 / Description of the feature validations: required: true - type: textarea id: suggested-solution attributes: label: 实现思路 / Suggested solution description: 实现此需求的解决思路。 validations: required: false - type: textarea id: additional-context attributes: label: 附件 / Additional context description: | 相关的任何其他上下文或截图。 Any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: tags: - 'v*' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Get Env uses: actions/github-script@v4 with: script: | const tag = process.env.GITHUB_REF.split('/').slice(-1)[0] const createChangelog = require('./scripts/changelog.js') const content = await createChangelog('https://github.com/'+process.env.GITHUB_REPOSITORY) core.exportVariable('CHANGELOG', content) core.exportVariable('VERSION', tag) - name: Build run: | rm -rf ./packages/sharelist-webdav yarn install yarn build-manage yarn build-web mkdir -p ./packages/sharelist/web/default mkdir -p ./packages/sharelist/manage wget https://raw.githubusercontent.com/linkdrive/plugins/master/list_full.json -O ./packages/sharelist/plugins.json cp -r ./packages/sharelist-web/dist/* ./packages/sharelist/web/default cp -r ./packages/sharelist-manage/dist/* ./packages/sharelist/manage yarn build-server - name: Release run: | cd ./packages/sharelist/build tar --transform='flags=r;s|sharelist-win-x64.exe|sharelist.exe|' -zcvf sharelist_windows_amd64.tar.gz sharelist-win-x64.exe tar --transform='flags=r;s|sharelist-macos-x64|sharelist|' -zcvf sharelist_macos_amd64.tar.gz sharelist-macos-x64 tar --transform='flags=r;s|sharelist-linux-x64|sharelist|' -zcvf sharelist_linux_amd64.tar.gz sharelist-linux-x64 tar --transform='flags=r;s|sharelist-linux-arm64|sharelist|' -zcvf sharelist_linux_arm64.tar.gz sharelist-linux-arm64 tar --transform='flags=r;s|sharelist-linuxstatic-armv7|sharelist|' -zcvf sharelist_linux_armv7.tar.gz sharelist-linuxstatic-armv7 gh release create ${{ env.VERSION }} -n "${{ env.NOTE }}" -t "${{ env.VERSION }}" ${{ env.FILES }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ env.VERSION }} NOTE: ${{ env.CHANGELOG }} TITLE: ${{ env.VERSION }} FILES: ./*.gz ================================================ FILE: .github/workflows/docker.yml ================================================ name: Docker on: push: # branches: # - next tags: - 'v*' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: yarn install - name: Pre Build run: | yarn build-web yarn build-manage mkdir -p ./packages/sharelist/web/default mkdir -p ./packages/sharelist/manage wget https://raw.githubusercontent.com/linkdrive/plugins/master/list_full.json -O ./packages/sharelist/plugins.json cp -r ./packages/sharelist-web/dist/* ./packages/sharelist/web/default cp -r ./packages/sharelist-manage/dist/* ./packages/sharelist/manage - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v1 - name: Build and push id: docker_build uses: docker/build-push-action@v2 with: context: ./packages/sharelist/ platforms: linux/amd64,linux/arm64 file: ./packages/sharelist/Dockerfile builder: ${{ steps.buildx.outputs.name }} push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/sharelist:${{ env.VERSION }} env: VERSION: next - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} ================================================ FILE: .gitignore ================================================ node_modules build/ dist/ /packages/sharelist/manage/ /packages/sharelist/theme/ /packages/sharelist/cache/ /packages/sharelist/web/ /packages/sharelist/plugin/ /packages/sharelist/example .vscode/ *.lock *.log *.exe package-lock.json .yarn/ plugins.json ================================================ FILE: .prettierrc.js ================================================ // https://prettier.io/docs/en/configuration.html module.exports = { //分号终止符 semi: false, //行尾逗号 trailingComma: "all", // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号) singleQuote: true, printWidth: 120, // 换行符 endOfLine: "auto", //缩进 default:2 tabWidth: 2, endOfLine: "auto", arrowParens: "avoid" } ================================================ FILE: .yarnrc ================================================ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 yarn-path ".yarn/releases/yarn-1.18.0.cjs" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-present, Reruin and Sharelist contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ShareList [![Build Status](https://github.com/reruin/sharelist/actions/workflows/ci.yml/badge.svg)](https://github.com/reruin/sharelist/actions/workflows/ci.yml) ShareList 是一个易用的网盘工具,支持快速挂载 GoogleDrive、OneDrive ,可通过插件扩展功能。 新版正在开发中,欢迎[提交反馈](https://github.com/reruin/sharelist/issues/new/choose)。[查看旧版](https://github.com/reruin/sharelist/tree/0.1)。 ## 文档 [查看文档](https://reruin.github.io/sharelist/docs/#/zh-cn/) ## 进度 - [x] 核心库支持 - [x] 新主题 - [x] 自定义插件开发 - [x] webdav - [x] 跨盘转移 - [x] 秒传 ## 支持情况 | | 下载 | 上传 | 列目录 | 创建目录 | 删除 | 重命名 | 远程移动 | hash | 秒传 | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | OneDrive | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | sha1 | x | GoogleDrive | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | md5 | x | Local File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | md5/sha1 | x | AliyunDrive | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | sha1 | ✓ | CaiYun | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | md5 | ✓ | CTCloud | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | md5 | ✓ | Baidu Netdisk | ✓ | x | ✓ | ✓ | ✓ | ✓ | ✓ | encrypt(md5) | ✓ | ## 安装 ```docker docker run -d -v /etc/sharelist:/sharelist/cache -p 33001:33001 --name="sharelist" reruin/sharelist:next ``` [release](https://github.com/reruin/sharelist/releases)下载二进制版。 ## 许可 [MIT](https://opensource.org/licenses/MIT) Copyright (c) 2018-present, Reruin ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/README.md ================================================ # ShareList [![Build Status](https://github.com/reruin/sharelist/actions/workflows/ci.yml/badge.svg)](https://github.com/reruin/sharelist/actions/workflows/ci.yml) ## Introduction ShareList is an easy-to-use netdisk tool which supports that quickly mount GoogleDrive and OneDrive, and extends functions with plugins. ## Documentation Check out [docs](https://reruin.github.io/sharelist/#/en/). ## License [Apache-2](http://www.apache.org/licenses/LICENSE-2.0) Copyright (c) 2018-present, Reruin ================================================ FILE: docs/_navbar.md ================================================ - Translations - [:uk: English](/) - [:cn: 中文](/zh-cn/) ================================================ FILE: docs/en/README.md ================================================ # ShareList [![Build Status](https://api.travis-ci.com/reruin/sharelist.svg?branch=master)](https://travis-ci.com/reruin/sharelist) ## Introduction ShareList is an easy-to-use netdisk tool which supports that quickly mount GoogleDrive and OneDrive, and extends functions with plugins. ## Documentation Check out [docs](https://reruin.github.io/sharelist/#/en/). ## License [Apache-2](http://www.apache.org/licenses/LICENSE-2.0) Copyright (c) 2018-present, Reruin ================================================ FILE: docs/en/_navbar.md ================================================ - Translations - [中文](/zh-cn/) - [English](/en/) ================================================ FILE: docs/en/_sidebar.md ================================================ * [Installation](en/installation.md) * [Configuration](en/configuration.md) * [Advance](en/advance.md) ================================================ FILE: docs/en/installation.md ================================================ # Installation Translation needed! ================================================ FILE: docs/index.html ================================================ ShareList Docs
================================================ FILE: docs/zh-cn/README.md ================================================ # ShareList [![Build Status](https://github.com/reruin/sharelist/actions/workflows/ci.yml/badge.svg)](https://github.com/reruin/sharelist/actions/workflows/ci.yml) ## 介绍 ShareList 是一个易用的网盘工具,支持快速挂载 GoogleDrive、OneDrive ,可通过插件扩展功能。 ## 许可 [Apache-2](http://www.apache.org/licenses/LICENSE-2.0) Copyright (c) 2018-present, Reruin ================================================ FILE: docs/zh-cn/_navbar.md ================================================ - Translations - [中文](/zh-cn/) - [English](/en/) ================================================ FILE: docs/zh-cn/_sidebar.md ================================================ * [安装](zh-cn/installation.md) * [配置](zh-cn/configuration.md) * [开发](zh-cn/dev.md) ================================================ FILE: docs/zh-cn/advance.md ================================================ ## 目录加密 在需加密目录内新建 ```.passwd``` 文件(此项可修改),```type```为验证方式,```data```为验证内容。 例如: ```yaml type: basic data: - 123456 - abcdef ``` 可使用密码```123456```,```abcdef```验证。 *** ## 获取文件夹ID 保持后台登录状态,回到首页列表,点击文件夹后的 '!' 按钮 可查看文件夹ID。 *** ## Nginx(Caddy)反向代理 使用反代时,请添加以下配置。 #### Nginx ```ini proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Range $http_range; proxy_set_header If-Range $http_if_range; proxy_no_cache $http_range $http_if_range; ``` 如果使用上传功能,请调整 nginx 上传文件大小限制。 ``` client_max_body_size 8000m; ``` #### Caddy ```ini header_upstream Host {host} header_upstream X-Real-IP {remote} header_upstream X-Forwarded-For {remote} header_upstream X-Forwarded-Proto {scheme} ``` ================================================ FILE: docs/zh-cn/configuration.md ================================================ ## 常规 访问 ```http://localhost:33001/@manage```,填写口令即可进入后台管理。 ### 后台管理 设置后台管理密码。默认 ```sharelist```。 ### 网站标题 设置网站标题。 ### 目录索引 默认启用。如果只提供下载功能,可禁用此项。 ### 展开单一挂载盘 默认启用。如果只有一个挂载盘,将默认展开。 ### 允许下载 默认启用。 ### 忽略路径 设置禁止访问的目录/文件路径。支持 [gitignore](http://git-scm.com/docs/gitignore) 表达式 ### 加密文件名 默认```.passwd```,修改此项自定义加密文件名。 ### WebDAV 路径 WebDAV路径。 ### WebDAV 代理 默认启用。 ### WebDAV 用户 默认 ```admin```。 ### WebDAV 密码 默认 ```sharelist```。 ### 自定义脚本 默认主题支持自定义脚本。可用于插入统计脚本。 ### 自定义样式 默认主题支持自定义样式。 ## 高级用法 在需加密目录内新建 ```.passwd``` 文件(此项可修改),```type```为验证方式,```data```为验证内容。 例如: ```yaml type: basic data: - 123456 - abcdef ``` 可使用密码```123456```,```abcdef```验证。 *** ### 获取文件夹ID 保持后台登录状态,回到首页列表,点击文件夹后的 '!' 按钮 可查看文件夹ID。 *** ### 反向代理 使用反代时,请添加以下配置。 #### Nginx ```ini proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Range $http_range; proxy_set_header If-Range $http_if_range; proxy_no_cache $http_range $http_if_range; ``` 如果使用上传功能,请调整 nginx 上传文件大小限制。 ``` client_max_body_size 8000m; ``` #### Caddy ```ini header_upstream Host {host} header_upstream X-Real-IP {remote} header_upstream X-Forwarded-For {remote} header_upstream X-Forwarded-Proto {scheme} ``` ================================================ FILE: docs/zh-cn/dev.md ================================================ ## 接口 ### 获取文件列表 ``` POST /api/drive/list ``` 请求参数 参数名 | 含义 | 是否必须 -|-|- path|路径|是 返回结果 名称 | 类型 | 说明 -|-|- id|string|目录唯一ID files|Array\|目录内容集合 files[0].id|string|文件ID files[0].name|string|文件名 files[0].size|number|文件大小 files[0].type|string|文件类型,目录(folder) 或 文件(file) files[0].ctime|number|文件创建时间戳 files[0].mtime|number|文件修改时间戳 files[0].path|string|文件相对路径 files[0].download_url|string|文件下载地址 ### 获取配置文件 ``` GET /api/setting ``` 请求参数 参数名 | 含义 | 是否必须 -|-|- token|后台口令|是 返回结果 名称 | 类型 | 说明 -|-|- data|object| data.token|string|后台口令 data.title|string|网站title data.theme|string|当前主题 data.theme_options|Array\|可用主题 data.index_enable|boolean|是否启用目录索引 data.ignores|Array\|忽略路径 data.acl_file|string|加密文件对应名称 data.webdav_path|string|WebDAV 路径 data.webdav_user|string|WebDAV 用户名 data.webdav_pass|string|WebDAV 密码 data.webdav_proxy|boolean|是否启用WebDAV代理 ### 重启应用 ``` PUT /api/reload?token={token} ``` ### 清除缓存 ``` PUT /api/cache/clear?token={token} ``` ### 导出配置 ``` GET /api/setting?raw=true&token={token} ``` # 主题开发 Sharelist 自定义主题存放路径为 ```cache/theme``` 目录。 # 插件开发 Sharelist 自定义插件路径存放路径为 ```cache/plugins``` 目录。 ### 资源命名 sharelist使用统一的资源命名方式,即 ```protocol://key/fid```。 字段|含义 -|- protocol|挂载协议,仅用于区分挂载类型 key|挂载标记,用于标记挂载实例 fid|资源的内部ID ### 辅助函数 插件在```onReady```时将接收```app```,```app```包含一些列辅助开发函数 函数名|用途 -|- request|用于完成 HTTP 请求 error|抛异常 createReadStream|流读取函数 getDrives|获取挂载信息 saveDrive|保存挂载信息 ### 插件功能 完整的插件应提供 方法|描述|必须功能 -|-|- list|列目录|✓ get|获取文件信息|✓ mkdir|新建目录 rm|删除 rename|重命名 mv|移动 upload|上传 ### 简单示例 ```js module.exports = class Driver { constructor() { this.name = 'TestPlugin' this.label = 'TestPlugin' //可被挂载 this.mountable = true //不使用缓存 this.cache = false //版本 this.version = '1.0' //协议名 this.protocol = 'test' //挂载需要的参数 this.guide = [ { key: 'path', label: '目录地址', type: 'string', required: true }, ] }, //初始化完毕,接收全局app 实例 onReady(app){ this.app = app }, list(id){ }, get(id){ }, mkdir(parent_id,name){ }, rm(id){ }, mv(){ } } ``` ================================================ FILE: docs/zh-cn/installation.md ================================================ # 安装 Sharelist支持多种安装方式。 ## Docker ```bash docker run -d -v /etc/sharelist:/sharelist/cache -p 33001:33001 --name="sharelist" reruin/sharelist:next ``` ## 二进制版 [release](https://github.com/reruin/sharelist/releases)下载二进制版。 ## Heroku 请 Fork [sharelist-heroku](https://github.com/reruin/sharelist-heroku/tree/next),然后在个人仓库下点 Deploy to HeroKu。 安装完成首次访问 `http://localhost:33001`地址,将进入默认界面。访问`http://localhost:33001/@manage` 进入后台管理,默认口令为 ```sharelist```。 ================================================ FILE: package.json ================================================ { "name": "sharelist-monorepo", "private": true, "workspaces": [ "packages/*" ], "engines": { "node": ">=16.0.0" }, "scripts": { "dev": "cd packages/sharelist && yarn dev", "build-server": "cd packages/sharelist && yarn pkg", "dev-web": "cd packages/sharelist-web && yarn dev", "dev-manage": "cd packages/sharelist-manage && yarn dev", "build-manage": "cd packages/sharelist-manage && yarn build", "build-web": "cd packages/sharelist-web && yarn build", "build": "node scripts/build.js", "lint": "eslint --ext .ts packages/*/src/**.ts", "format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"", "release": "node scripts/release.js", "ci": "cd packages/sharelist && yarn release", "changelog": "node scripts/changelog.js", "pkg": "cd packages/sharelist && yarn pkg-local" }, "devDependencies": { "@babel/cli": "^7.16.8", "@babel/core": "^7.16.7", "@babel/plugin-proposal-optional-chaining": "^7.16.7", "chalk": "^4.1.2", "conventional-changelog-cli": "^2.1.1", "cross-env": "^7.0.3", "eslint": "^7.28.0", "execa": "^5.1.1", "minimist": "^1.2.5", "nodemon": "^2.x", "pkg": "^5.3.1", "prompts": "^2.4.1", "semver": "^7.3.5" } } ================================================ FILE: packages/sharelist/CHANGELOG.md ================================================ ## [0.4.4](https://github.com/reruin/sharelist/compare/v0.4.3...v0.4.4) (2025-06-13) ## [0.4.3](https://github.com/reruin/sharelist/compare/v0.4.2...v0.4.3) (2025-06-12) ## [0.4.2](https://github.com/reruin/sharelist/compare/v0.4.1...v0.4.2) (2025-06-12) ## 0.4.1 (2025-06-12) ### Bug Fixes * fix some bugs ([23cd7f9](https://github.com/reruin/sharelist/commit/23cd7f99d5af27b08cba08109c2107a3a6d04089)) * **guide:** fix wrong function call ([01e5a78](https://github.com/reruin/sharelist/commit/01e5a78f54b59ddcb8ac04b2d1c1297710f5946d)) * **plugin:** baidu guide([#564](https://github.com/reruin/sharelist/issues/564)) ([219b524](https://github.com/reruin/sharelist/commit/219b524e0604751fae84eca3c65b350a479817eb)) * **web:** fix bugs([#723](https://github.com/reruin/sharelist/issues/723),[#747](https://github.com/reruin/sharelist/issues/747)) ([10d2550](https://github.com/reruin/sharelist/commit/10d25502248811a2d313d442f40592c66a5cd443)) ### Features * support custom script/css ([8af3902](https://github.com/reruin/sharelist/commit/8af390226a63373477d597a6f6b231e1c34f6cfa)) * **webdav:** auth ([2499979](https://github.com/reruin/sharelist/commit/2499979dcd8392864f505268411dbce15cd810dc)) * **webdav:** support option configuration of webdav ([d667a83](https://github.com/reruin/sharelist/commit/d667a830f8008a857d6ae827213d76992edbe306)) ## [0.3.15](https://github.com/reruin/sharelist/compare/v0.3.14...v0.3.15) (2022-01-05) ## [0.3.14](https://github.com/reruin/sharelist/compare/v0.3.13...v0.3.14) (2022-01-05) ## [0.3.13](https://github.com/reruin/sharelist/compare/v0.3.12...v0.3.13) (2022-01-04) ### Bug Fixes * fix some bugs ([23cd7f9](https://github.com/reruin/sharelist/commit/23cd7f99d5af27b08cba08109c2107a3a6d04089)) ## [0.3.12](https://github.com/reruin/sharelist/compare/v0.3.11...v0.3.12) (2021-12-29) ### Bug Fixes * **web:** fix bugs([#723](https://github.com/reruin/sharelist/issues/723),[#747](https://github.com/reruin/sharelist/issues/747)) ([10d2550](https://github.com/reruin/sharelist/commit/10d25502248811a2d313d442f40592c66a5cd443)) ### Features * support custom script/css ([8af3902](https://github.com/reruin/sharelist/commit/8af390226a63373477d597a6f6b231e1c34f6cfa)) ## [0.3.11](https://github.com/reruin/sharelist/compare/v0.3.10...v0.3.11) (2021-11-05) ## [0.3.10](https://github.com/reruin/sharelist/compare/v0.3.9...v0.3.10) (2021-11-01) ## [0.3.9](https://github.com/reruin/sharelist/compare/v0.3.8...v0.3.9) (2021-10-23) ## [0.3.8](https://github.com/reruin/sharelist/compare/v0.3.7...v0.3.8) (2021-10-19) ## [0.3.7](https://github.com/reruin/sharelist/compare/v0.3.6...v0.3.7) (2021-10-14) ### Features * **webdav:** support option configuration of webdav ([d667a83](https://github.com/reruin/sharelist/commit/d667a830f8008a857d6ae827213d76992edbe306)) ## [0.3.6](https://github.com/reruin/sharelist/compare/v0.3.5...v0.3.6) (2021-10-12) ## [0.3.6](https://github.com/reruin/sharelist/compare/v0.3.5...v0.3.6) (2021-10-12) ## [0.3.5](https://github.com/reruin/sharelist/compare/v0.3.4...v0.3.5) (2021-10-11) ## [0.3.4](https://github.com/reruin/sharelist/compare/v0.3.3...v0.3.4) (2021-10-10) ## [0.3.3](https://github.com/reruin/sharelist/compare/v0.3.2...v0.3.3) (2021-10-10) ## [0.3.2](https://github.com/reruin/sharelist/compare/v0.3.1...v0.3.2) (2021-10-09) ### Features * **webdav:** auth ([2499979](https://github.com/reruin/sharelist/commit/2499979dcd8392864f505268411dbce15cd810dc)) ## [0.3.1](https://github.com/reruin/sharelist/compare/v0.3.0...v0.3.1) (2021-10-09) # [0.3.0](https://github.com/reruin/sharelist/compare/v0.2.4...v0.3.0) (2021-10-09) ## [0.2.3](https://github.com/reruin/sharelist/compare/v0.2.2...v0.2.3) (2021-08-21) ### Bug Fixes * **guide:** fix wrong function call ([01e5a78](https://github.com/reruin/sharelist/commit/01e5a78f54b59ddcb8ac04b2d1c1297710f5946d)) ## [0.2.2](https://github.com/reruin/sharelist/compare/v0.2.1...v0.2.2) (2021-08-21) ## 0.2.1 (2021-08-14) ================================================ FILE: packages/sharelist/Dockerfile ================================================ FROM node:20-alpine LABEL maintainer=reruin ADD . /sharelist/ WORKDIR /sharelist VOLUME /sharelist/cache RUN npm install --production ENV HOST 0.0.0.0 ENV PORT 33001 EXPOSE 33001 CMD ["npm", "start"] ================================================ FILE: packages/sharelist/LICENSE ================================================ MIT License Copyright (c) 2018-present, Reruin and Sharelist contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/sharelist/README.md ================================================ # ShareList Server ================================================ FILE: packages/sharelist/app/config.js ================================================ const path = require('path') const env = { baseDir: path.join(__dirname, '../'), cacheDir: path.join(!process.pkg ? __dirname : process.execPath, '../cache'), pkg: !!process.pkg, dev: process.env.NODE_ENV === 'dev' } module.exports = { env, cacheDir: env.cacheDir, pluginDir: path.join(env.cacheDir, 'plugin'), themeDir: [path.join(env.baseDir, 'web'), path.join(env.cacheDir, 'theme')], manageDir: path.join(env.baseDir, 'manage'), defaultPluginsFile: path.join(env.baseDir, 'plugins.json'), } ================================================ FILE: packages/sharelist/app/main.js ================================================ const createSharelist = require('./modules/core') const createWebDAV = require('./modules/webdav') const createServer = require('./modules/server') const createGuide = require('./modules/guide') const config = require('./config') const path = require('path') const fs = require('fs') const install = async () => { let pluginDir = config.pluginDir if (fs.existsSync(pluginDir) == false) { fs.mkdirSync(pluginDir, { recursive: true }) try { let plugins = JSON.parse(fs.readFileSync(config.defaultPluginsFile, 'utf-8')) for (let i of plugins) { console.log(i.name) let filename = path.join(pluginDir, `${i.namespace}.js`) fs.writeFileSync(filename, i.script) } } catch (e) { console.log('Extract default plugins error:', e) } } fs.mkdirSync(path.join(config.cacheDir, 'theme'), { recursive: true }) } const bootstrap = async () => { await install() const sharelist = await createSharelist(config) const server = createServer(sharelist, config) server.use(createWebDAV(sharelist)) server.use(createGuide(sharelist)) server.start() return { app: server.app, port: config.port } } module.exports = bootstrap ================================================ FILE: packages/sharelist/app/modules/command/index.js ================================================ /** * 文件名寻址操作函数 * @param {*} v * @returns */ const parsePath = v => v.replace(/(^\/|\/$)/g, '').split('/').map(decodeURIComponent).filter(Boolean) const getRange = (r, total) => { if (r) { let [, start, end] = r.match(/(\d*)-(\d*)/); start = start ? parseInt(start) : 0 end = end ? parseInt(end) : total - 1 return { start, end } } } const createHeaders = (data, { maxage, immutable, range } = { maxage: 0, immutable: false }) => { let fileSize = data.size let fileName = data.name let headers = {} headers['Last-Modified'] = new Date(data.mtime).toUTCString() if (range) { let { start, end } = range headers['Content-Range'] = `bytes ${start}-${end}/${fileSize}` headers['Content-Length'] = end - start + 1 headers['Accept-Ranges'] = 'bytes' } else { header['Content-Range'] = `bytes 0-${fileSize - 1}/${fileSize}` headers['Content-Length'] = fileSize } headers['Content-Disposition'] = `attachment;filename=${encodeURIComponent(fileName)}` return headers } const createCommand = (driver, { useProxy, baseUrl } = {}) => ({ async ls(path) { let p = path.replace(/(^\/|\/$)/g, '') let data = await driver.list({ paths: p ? p.split('/').map(decodeURIComponent) : [], ignoreInterceptor: true, query: { pagination: false } }) if (data.files?.length > 0) { data.files .sort((a, b) => (a.type == 'folder' ? -1 : 1)) .forEach((i) => { if (i.type == 'file') { i.download_url = baseUrl + '/api/drive/get?download=true&id=' + encodeURIComponent(i.id) } }) } return data }, async stat(path) { //console.log('stat', parsePath(path),) try { return await driver.stat(parsePath(path)) } catch (error) { return { error } } }, async get(path, options) { let data = await driver.get({ paths: parsePath(path) }) if (!options.reqHeaders) options.reqHeaders = {} delete options.reqHeaders.connection options.reqHeaders['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' if (data && data.download_url && !data.extra.proxy && !useProxy()) { return { status: 302, body: data.download_url } } else { let range = getRange(options.reqHeaders.range, data.size) || { start: 0, end: data.size ? (data.size - 1) : '' } let { stream, status, headers, enableRanges = false } = await driver.createReadStream(data.id, range) let isReqRange = !!options.reqHeaders.range if (stream) { let options = enableRanges ? { range } : {} let resHeaders = headers || createHeaders(data, options) return { body: stream, status: status || (isReqRange && enableRanges ? 206 : 200), headers: resHeaders } } } }, async upload(path, stream, { size }) { stream.pause?.() let paths = parsePath(path) let name = paths.pop() let data = await driver.stat(paths) console.log(data) let existData = await driver.stat([...paths, name]) if (existData) { await driver.rm(existData.id) } if (!data.id) { return { error: { code: 404 } } } let ret = await driver.upload(data.id, stream, { name, size }) if (!ret) { return { error: { code: 500 } } } else { return ret } }, async mkdir(path) { let paths = parsePath(path) let name = paths.pop() let parentData = await driver.stat(paths) return await driver.mkdir(parentData.id, name) }, async rm(path) { let paths = parsePath(path) let data = await driver.stat(paths) return await driver.rm(data.id) }, // /d/e/1.txt -> /d async mv(path, destPath, copy) { // The destination path can NOT be in the source path (include the same path) // e.g. /a/b => /a/b , /a/b => /a/b/c , ( /a/b => /a/b1, /a/b => /a/b1/c console.log('mv', path, destPath) let paths = parsePath(path) let destPaths = parsePath(destPath) if ((destPaths.join('/') + '/').startsWith(paths.join('/' + '/'))) throw { code: 409 } let data = await driver.stat(paths) if (!data?.id) throw { code: 404 } let srcId = data.id let isSameParent = paths.length == destPaths.length && paths.slice(0, -1).join('/') == destPaths.slice(0, -1).join('/') // rename if (isSameParent) { if (paths.slice(-1)[0] == destPaths.slice(-1)[0]) throw { code: 409 } if (!copy) { await driver.rename(srcId, destPaths.slice(-1)[0]) } } let dest = await driver.stat(destPaths) let destId, destName, srcName = paths.pop() //if destination exists if (dest?.id) { // destination must be a folder if (dest.type != 'folder') throw { code: 409 } destId = dest.id } // destination does not exist else { let destParent = await driver.stat({ paths: destPaths.slice(0, -1) }) //dest parent must be a folder if (destParent?.type != 'folder') throw { code: 404 } destName = destPaths.pop() destId = destParent.id } let isSameDrive = await driver.isSameDrive(srcId, destId) if (!isSameDrive) throw ({ code: 501 }) let options = { copy } //rename if (destName && srcName != destName) options.name = destName await driver.mv(srcId, destId, options) return { status: 201 } } }) module.exports = (app) => { app.addSingleton('command', () => { console.log(';>>>command') let ret = createCommand(app.sharelist.driver) console.log(ret) return ret }) } ================================================ FILE: packages/sharelist/app/modules/core/cache.js ================================================ const createDB = require('./db') module.exports = (path) => { let { data, save } = createDB(path) const get = (id) => { if (id === undefined) return data let ret = data[id] if (ret) { if (Date.now() > ret.expired_at) { delete data[id] } else { return ret.data } } } const set = (id, value, max_age) => { data[id] = { data: value, expired_at: Date.now() + max_age } save() return value } const clear = (key) => { if (key) { delete data[key] } else { for (let key in data) { delete data[key] } } save() } const remove = (key) => { delete data[key] save() } // remove expired data for (let key in data) { get(key, data) save() } return { get, set, remove, clear, save } } ================================================ FILE: packages/sharelist/app/modules/core/config.js ================================================ const createDB = require('./db') const qs = require('querystringify') const { URL } = require('url') const { watch } = require('@vue-reactivity/watch') const { reactive } = require('@vue/reactivity') const { nanoid } = require('nanoid') const defaultConfig = { token: 'sharelist', proxy_enable: false, index_enable: true, expand_single_disk: true, // fast_mode: true, max_age_dir: 15 * 60 * 1000, max_age_file: 5 * 60 * 1000, // max_age_download: 0, theme: 'default', ignores: [], acl_file: '.passwd', max_age_download_sign: 'sl_' + Date.now(), anonymous_upload_enable: false, anonymous_download_enable: true, webdav_path: '', //代理路径 proxy_paths: [], proxy_server: '', proxy_override_content_type: 1, webdav_proxy: true, webdav_user: 'admin', webdav_pass: 'sharelist', ocr_server: '', drives: [], manage_path: '/@manage/', proxy_url: '', plugin_source: 'unpkg', default_ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36' } exports.openConfigKey = ['manage_path'] exports.defaultConfigKey = Object.keys(defaultConfig) const decode = (p) => { let hasProtocol = p.includes('://') if (!hasProtocol) p = 'sharelist://' + p let data = new URL(p) let protocol = data.protocol.replace(':', '') let path = decodeURIComponent(data.pathname || '') let result = { protocol, key: data.host, } if (path) path = path.replace(/^\/+/, '') if (hasProtocol) result.protocol = data.protocol.split(':')[0] const config = { root: path } for (const [key, value] of data.searchParams) { config[key] = value } result.config = config return result } const encode = (data) => { let { protocol, config: { key, root, ...options } } = data let pathname = root === undefined || root == '' ? '/' : '/' + root let search = qs.stringify(options) if (search) search = '?' + search let ret = `${key || ''}${pathname}${search}` if (protocol) ret = `${protocol}://${ret}` return ret } exports.createConfig = (path) => { let { data, save } = createDB(path, { autoSave: false }, { ...defaultConfig }) let proxyData = reactive({ ...data, drives: data.drives.map(i => ({ name: i.name, id: i.id || nanoid(), ...decode(i.path) })) }) watch(proxyData, (nv) => { Object.keys(nv).forEach((key) => { if (key == 'drives') { data.drives = nv.drives.map(i => ({ name: i.name, id: i.id, path: encode(i) })) } else { data[key] = nv[key] } }) save() }, { deep: true }) return proxyData } ================================================ FILE: packages/sharelist/app/modules/core/db.js ================================================ const path = require('path') const fs = require('fs') const writeFileAtomic = require('write-file-atomic') const mkdir = function (p) { if (fs.existsSync(p) == false) { mkdir(path.dirname(p)) fs.mkdirSync(p) } } const base64 = { encode: (v) => Buffer.from(v).toString('base64'), decode: (v) => Buffer.from(v, 'base64').toString(), } const merge = function (dst, src) { for (let key in src) { if (!(key in dst)) { dst[key] = src[key] continue } else { if (typeof src[key] == 'object' || Array.isArray(src[key])) { merge(dst[key], src[key]) } else { dst[key] = src[key] } } } return dst } const getData = (path, options = {}) => { try { let data = fs.readFileSync(path, 'utf8') if (options.base64) { data = base64.decode(data) } return JSON.parse(data) } catch (error) { //if it doesn't exist or permission error if (error.code === 'ENOENT' || error.code === 'EACCES') { return {} } //invalid JSON if (error.name === 'SyntaxError') { writeFileAtomic.sync(path, '') return {} } throw error } } const setData = (filepath, { base64 } = {}, value) => { try { mkdir(path.dirname(filepath)) value = JSON.stringify(value) if (base64) { value = base64.encode(value) } writeFileAtomic.sync(filepath, value) } catch (error) { throw error; } } const createdb = (path, { autoSave, debug } = {}, defaults = {}) => { let data = merge(defaults, getData(path)) let handler const save = (nv) => { if (debug) console.log('db changed', nv) if (handler) { clearImmediate(handler) } handler = setImmediate(() => { setData(path, {}, nv || data) handler = null }) } return { data, save } } module.exports = createdb ================================================ FILE: packages/sharelist/app/modules/core/http.js ================================================ class http { static options = { protocol: "http", singleton: true, mountable: false } constructor(app, config) { this.app = app this.config = config } pwd(id) { let name = id.split('/').pop() return [{ id, name, type: 'file' }] } async get(id) { let url = id let size = await this.getFileSize(url) let name = url.split('/').pop() return { id, size, name, type: 'file', ctime: Date.now(), mtime: Date.now(), download_url: url, extra: {} } } async list(id) { } async getFileSize(url, headers = {}) { try { let controller = new AbortController() let { headers: resHeaders } = await this.app.request(url, { signal: controller.signal, headers, responseType: 'stream' }) controller.abort() let size = +resHeaders['content-length'] return size } catch (e) { console.log(e) return null } } // async createReadStream({ id, options = {} } = {}) { // let url = id // let size = await this.getFileSize(url) // let readstream = request({ url: decodeURIComponent(url), method: 'get' }) // return wrapReadableStream(readstream, { size }) // } } class https extends http { static options = { protocol: "https", singleton: true, mountable: false } constructor(app, config) { super(app, config) this.protocol = 'https' } } exports.HTTPDriver = { name: 'http', hash: 'sharelist.http', module: { driver: http } } exports.HTTPSDriver = { name: 'https', hash: 'sharelist.https', module: { driver: https } } ================================================ FILE: packages/sharelist/app/modules/core/index.js ================================================ const path = require('path') const { nanoid } = require('nanoid') const { request, createPluginLoader } = require('@sharelist/core') const createCache = require('./cache') const { createConfig, defaultConfigKey } = require('./config') const utils = require('./utils') const { createPlugin } = require('./plugin') const createTheme = require('./theme') const { createTransfer } = require('./task') module.exports = async (options) => { const config = createConfig(path.join(options.cacheDir, 'config.json')) const cache = createCache(path.join(options.cacheDir, 'cache.json')) const plugin = createPlugin({ pluginSource: () => config.plugin_source, pluginDir: path.join(options.cacheDir, 'plugin'), onUpdate(id) { //driver has been changed. driver.load(plugin.load()) }, onRemove(hash) { console.log('onremove', hash) driver.unload([hash]) } }) // plugins manager const driver = await createPluginLoader({ config, plugins: plugin.load(), cache }) const theme = createTheme(options.themeDir) const transfer = createTransfer(options.cacheDir, driver, request) const files = (...rest) => utils.getFiles(driver, config, ...rest) const file = (...rest) => utils.getFile(driver, config, ...rest) const getDownloadUrl = (...rest) => utils.getDownloadUrl(driver, config, ...rest) const getPathById = (...rest) => utils.getPathById(driver, config, ...rest) const getContent = (...rest) => driver.createReadStream(...rest) const getThemeFile = (file) => theme.getFile(file, config.theme) const checkAccess = (token) => config.token && config.token === token const reload = async () => { await driver.load(plugin.load()) } const setDrives = (data) => { for (let i of data) { if (!i.id) { i.id = nanoid() } } config.drives = data driver.loadConfig() } //if config update/create // watch( // () => config.drives, // (nv, ov) => { // console.log('drive config changed') // driver.loadConfig() // }, // ) return { config, cache, driver, defaultConfigKey, plugin, theme, files, file, getPathById, getContent, getDownloadUrl, getThemeFile, transfer, checkAccess, request, reload, setDrives } } ================================================ FILE: packages/sharelist/app/modules/core/plugin.js ================================================ const { nanoid } = require('nanoid') const crypto = require('crypto') const fs = require('fs') const path = require('path') const compareSemver = require('semver-compare-lite') const md5 = content => crypto.createHash('md5').update(content).digest("hex") const { request } = require('@sharelist/core') const { HTTPDriver, HTTPSDriver } = require('./http') const plugins_mirror = { 'github': 'https://raw.githubusercontent.com/linkdrive/plugins/master', 'unpkg': 'https://unpkg.com/@linkdrive/plugins', 'eleme': 'https://npm.elemecdn.com/@linkdrive/plugins' } const parseMeta = (filepath, isPath = true, dir) => { const content = isPath ? fs.readFileSync(filepath, 'utf-8') : filepath const metaContent = content.match(/(?<===Sharelist==)[\w\W]+(?===\/Sharelist==)/)?.[0] const meta = { hash: md5(content) } if (metaContent) { let pairs = metaContent.split(/[\r\n]/).filter(Boolean).map(i => i.match(/(?<=@)([^\s]+?)\s+(.*)/)).filter(Boolean) for (let i of pairs) { meta[i[1]] = i[2] } } if (isPath) { if (!meta.name) meta.name = path.basename(filepath).replace(/\.js$/i, '') // if(meta.name) meta.id = md5(meta.name || path) meta.path = filepath } else { let name = nanoid() meta.id = md5(meta.name || name) if (dir) meta.path = path.join(dir, name + '.js') } return meta } const clearNodeCache = (id) => { delete require.cache[id] Object.keys(module.constructor._pathCache).forEach(function (cacheKey) { if (cacheKey.indexOf(id) > -1) { delete module.constructor._pathCache[cacheKey]; } }); } class Plugin { constructor(options) { this.options = options this.plugins = {} this.inited = false } /** * extract plugin meta */ scanMeta() { let { options, plugins } = this let dirs = [options.pluginDir].filter(Boolean) let newLoad = [] for (let dir of dirs) { try { let files = fs.readdirSync(dir) for (let i of files) { let filepath = path.join(dir, i) let file = fs.statSync(filepath) if (file.isFile() && /\.js$/i.test(i)) { let meta = parseMeta(filepath) //plugin content has changed //test // meta.hash = Math.random() + '' if (plugins[meta.id]?.hash != meta.hash) { newLoad.push(meta) plugins[meta.id] = meta } } } } catch (e) { console.log(e) } } this.inited = true return newLoad } load() { console.log('load plugins') const plugins = this.scanMeta() const ret = [] for (let i of plugins) { let item = { name: i.name, hash: i.hash } try { clearNodeCache(require.resolve(i.path)) item.module = require(i.path) // item.code = fs.readFileSync(i.path) ret.push(item) } catch (e) { console.log(e) } } return [HTTPDriver, HTTPSDriver, ...ret] } get(id) { if (!this.inited) { this.scanMeta() } if (id) { return this.plugins[id] } else { return Object.values(this.plugins) } } set(id, data) { let meta = this.plugins[id], filepath let { pluginDir } = this.options if (!meta) { const newMeta = parseMeta(data, false, pluginDir) if (!newMeta.name) { throw new Error('invalid script / 脚本无效 - 缺少名称') } meta = newMeta filepath = newMeta.path } else { filepath = meta.path } fs.writeFileSync(filepath, data) this.options?.onUpdate(meta) } async createFromUrl(url) { console.log('Download plugin:', url) let res = await request(url, { responseType: 'text' }) this.set(null, res.data) } async getFromStore() { const plugins = this.get() console.log('SOURCE: ' + plugins_mirror[this.options.pluginSource()]) let { data } = await request(plugins_mirror[this.options.pluginSource()] + '/list.json?t=' + Date.now(), { // responseType: 'text' // headers: { // 'cache-control': 'no-cache', // 'pragma': 'no-cache' // } }) // data = JSON.parse(data) // console.log(data) data.forEach(i => { if (i.updateURL.indexOf('raw.githubusercontent.com') >= 0) { if (i.namespace) { if (plugins.find(j => j.namespace == i.namespace)) { i.installed = true } } i.updateURL = this.replaceSource(i.updateURL)//.replace('/master', '@master') if (i.namespace?.startsWith('http') && !i.supportURL) { i.supportURL = i.namespace } } }) return data } remove(id) { let meta = this.plugins[id] if (meta.path) { fs.unlinkSync(meta.path) delete this.plugins[id] this.options?.onRemove(meta.hash) } else { throw new Error('invalid script / 脚本无效') } } replaceSource(url) { console.log(url, plugins_mirror.github) return url.replace(plugins_mirror.github, plugins_mirror[this.options.pluginSource()]) } async upgrade(id) { let meta = this.plugins[id] if (meta?.id && meta.updateURL) { console.log('Upgrade Plugin:', meta.updateURL, this.replaceSource(meta.updateURL)) let { data } = await request(this.replaceSource(meta.updateURL), { responseType: 'text' }) if (!data) { throw new Error('没有获取到有效内容 / Invalid content') } let newmeta = parseMeta(data, false) if (meta.version && compareSemver(newmeta.version, meta.version) == 1) { console.log('[UPDATE PLUGIN]' + meta.name + ` ${meta.version} --> ${newmeta.version}`) this.set(meta.id, data) } else { throw new Error('已是最新版本') } } else { throw new Error('无法完成更新: 插件不存在 或 未设置更新地址.Unable to complete update: the plugin does not set the update url') } } getSources() { return Object.keys(plugins_mirror) } } exports.createPlugin = (options) => { return new Plugin(options) } exports.pluginConfigKey = ['search', 'hash', 'isRoot', 'multiThreading', 'readonly'] ================================================ FILE: packages/sharelist/app/modules/core/task.js ================================================ /** * type ITask = { * total:number, * success:number, * error:number, * totalSize:number, * status: STATUS * } */ const path = require('path') const { PassThrough, pipeline } = require('stream') const fs = require('fs') const createDb = require('./db') const { md5, isUrl } = require('./utils') const createAsyncCtrl = (beforeReject) => { let error let controller = new AbortController() let promise = new Promise((_, reject) => { error = (e) => { controller.abort() reject(e) } }) let run = (p) => Promise.race([p, promise]) return { error, run, signal: controller.signal } } const STATUS = { INIT: 1, //1 正在生成任务(解析文件) INIT_ERROR: 2, //2 解析文件过程发生错误 PROGRESS: 3, //3 正在复制 SUCCESS: 4, //4 操作完成 DONE_WITH_ERROR: 5,//5.操作完成 但发生部分完成 ERROR: 6,//6 失败 PAUSE: 7,//已暂停 } const genKey = (input) => md5(input).substring(8, 24) const parsePaths = v => v.replace(/(^\/|\/$)/g, '').split('/').map(decodeURIComponent).filter(Boolean) const sleep = (t = 0) => new Promise((r) => { setTimeout(r, t) }) const statsStream = (stream, cb) => { let loaded = 0, lastTime = Date.now(), chunkloaded = 0 stream.on('data', (chunk) => { loaded += chunk.length chunkloaded += chunk.length let timePass = Date.now() - lastTime if (timePass >= 1000) { cb(loaded, chunkloaded * 1000 / timePass) chunkloaded = 0 lastTime = Date.now() } }) } const ignoreStream = (readStream, length) => { let stream = new PassThrough() let count = 0 const onData = (chunk) => { count += chunk.length if (count >= length) { readStream.off('data', onData) stream.write(chunk.slice(count - length)) readStream.pipe(stream) } } readStream.on('data', onData) readStream.resume() return stream } exports.createTransfer = (cacheDir, driver, request) => { const TAG = 'transfer' const basePath = path.join(cacheDir, TAG) if (fs.existsSync(basePath) == false) { fs.mkdirSync(basePath) } const { data: tasksMap } = createDb(path.join(basePath, `list.json`), { autoSave: true, debug: false }) const worker = {} //取得目标目录,没有则创建 const changeDir = async (dest) => { let paths = dest.split('/'), nonExistDir = [] let parent while (true) { try { parent = await driver.stat(paths) if (parent) { break; } else { nonExistDir.unshift(paths.pop()) } } catch (e) { console.log('stat error', e) nonExistDir.unshift(paths.pop()) } } for (let i = 0; i < nonExistDir.length; i++) { parent = await driver.mkdir(parent.id, nonExistDir[i]) } return parent } const pickup = (file, parent) => { let r = { id: file.id, name: file.name, size: file.size, dest: parent } if (file.extra) { if (file.extra.md5) { // r.md5 = file.extra.md5 r.hash = { md5: file.extra.md5 } // r.hash_type = 'md5' } else if (file.extra.sha1) { // r.sha1 = file.extra.sha1 r.hash = { sha1: file.extra.sha1 } // r.hash_type = 'sha1' } } if (!r.size) r.lazy = true return r } const create = async (src, dest, idMode = false, { conflictBehavior = 1, threadNum = 1 } = {}) => { let srcPaths = [], destPaths = [], srcId, destId if (idMode) { srcId = src destId = dest srcPaths = (await driver.pwd(src)).map(i => i.name) destPaths = (await driver.pwd(dest)).map(i => i.name) } else { srcPaths = parsePaths(src) destPaths = parsePaths(dest) } console.log('move:', srcPaths, '==>', destPaths) const taskId = genKey(srcPaths.join('/') + '->' + destPaths.join('/')) //相同 if (tasksMap[taskId]) { let task = tasksMap[taskId] // console.log('The same task exists.', task) //if (task.status == STATUS.SUCCESS) { // delete tasksMap[taskId] //} else { throw { message: 'Task already exists / 存在相同的任务', code: 429 } //} } tasksMap[taskId] = { id: taskId, count: 0, status: STATUS.INIT, src: srcPaths.join('/'), srcId, dest: destPaths.join('/'), destId, size: 0, error: 0, success: 0, loaded: 0, currentLoaded: 0, speed: 0, index: 0, inited: false, currentDir: '', conflictBehavior, threadNum, retry: 0 } createParseTask(taskId) } //读取目录 const createParseTask = async (taskId) => { //当前任务的文件列表 let { save: saveFiles } = createDb(path.join(basePath, `${taskId}.json`), { autoSave: false }, []) //任务元数据 let { data: taskData } = createDb(path.join(basePath, `${taskId}_data.json`), { autoSave: true }, { error: [], retried: [] }) let files = [] let abortSignal = false let srcPaths = tasksMap[taskId].src.split('/').filter(Boolean) let srcId = tasksMap[taskId].srcId let destPaths = tasksMap[taskId].dest.split('/').filter(Boolean) worker[taskId] = { files, data: taskData, cancel: function cancel() { abortSignal = true } } //准备阶段,读取源目录信息 try { // console.log('get', srcId, srcPaths) let res = await driver.get({ id: srcId, paths: srcPaths }, { enableCache: false, more: true }) if (abortSignal) return if (res.type == 'file') { files.push( pickup(res, destPaths.join('/')) ) } else { let dirs = [[res.id, [...destPaths, srcPaths.pop()].join('/')]] while (dirs.length) { let [id, destPath] = dirs.shift() let children = (await driver.list({ id })).files let subfiles = children.filter(i => i.type != 'folder').map((i) => pickup(i, destPath)) if (subfiles) { files.push( ...subfiles ) } dirs.push(...children.filter(i => i.type == 'folder').map(i => [i.id, destPath ? `${destPath}/${i.name}` : i.name])) if (abortSignal) { return } } } if (abortSignal) return let fileTask = files.filter(i => !!i.id) tasksMap[taskId].count = fileTask.length tasksMap[taskId].size = fileTask.reduce((t, c) => t + c.size, 0) tasksMap[taskId].inited = true //tasksMap[taskId].status = STATUS.PROGRESS //保存到文件 saveFiles(files) createTransferTask(taskId) } catch (e) { console.trace(e) tasksMap[taskId].status = STATUS.INIT_ERROR tasksMap[taskId].message = e?.message } } const createTransferTask = async (taskId) => { let abortSignal = false let currentDirData const setState = (state) => { let { data } = worker[taskId] let { index, size: totalSize, count } = tasksMap[taskId] let { $stats, ...rest } = state let key = `${taskId}@${index}` if (Object.keys(rest).length) { data[key] = rest } // 单独处理状态报告 if ($stats) { let { loaded, speed, total } = $stats tasksMap[taskId].currentLoaded = loaded tasksMap[taskId].progress = totalSize ? (loaded / totalSize) : ((index + loaded / total) / count) tasksMap[taskId].speed = speed } } const streamCreater = (id, ctrl, fileData) => async ({ start, end, state, supportStatsReport }) => { let { stream: readStream, enableRanges } = await driver.createReadStream(id, { start, end, signal: ctrl.signal, }) //! 如果传入流 无法进行续传,则等待该流到达指定位置 if (!enableRanges && start != 0) { readStream = ignoreStream(readStream, start) } // 默认通过传入流进行间接计算 状态。但: // 1. 当挂载源支持多线程时,应自行实现状态探针。 if (!supportStatsReport) { statsStream(readStream, (loaded, speed) => { setState({ '$stats': { loaded, speed, total: fileData } }) }) } // work has been removed. if (!worker[taskId]) { return } readStream.once('error', ctrl.error) if (tasksMap[taskId] && start) { tasksMap[taskId].currentLoaded = start } if (state) setState(state) return readStream } const next = async () => { if (abortSignal) return const { files, data } = worker[taskId] let { index: taskIndex, conflictBehavior, retry, threadNum } = tasksMap[taskId] //finish if (taskIndex >= files.length || taskIndex == -1) { tasksMap[taskId].status = tasksMap[taskId].error > 0 ? (tasksMap[taskId].error == tasksMap[taskId].count ? STATUS.ERROR : STATUS.DONE_WITH_ERROR) : STATUS.SUCCESS return } console.log('currentIndex', taskIndex) let file = files[taskIndex] if (file.lazy) { let res = await driver.get({ id: file.id }, { enableCache: false, more: true }) file = Object.assign({}, file, res) } let { id, name, size, dest, hash, hash_type } = file //执行进入目录 if ((dest && dest != tasksMap[taskId].currentDir) || !currentDirData) { currentDirData = await changeDir(dest) tasksMap[taskId].currentDir = dest } //所有上传任务 都需要指明父目录 if (!currentDirData?.id) return let key = `${taskId}@${taskIndex}` const ctrl = createAsyncCtrl() //当前任务的文件 tasksMap[taskId].current = name worker[taskId].cancel = function cancel() { abortSignal = true ctrl.error({ type: 'aborted' }) } try { await ctrl.run(driver.upload(currentDirData.id, streamCreater(id, ctrl, file), { name, size, conflictBehavior, threadNum, hash: hash || {}, hash_type, signal: ctrl.signal, state: data[key], setState })) tasksMap[taskId].success++ } catch (e) { console.log(e) // 用户主动 abort 导致的异常 不计入异常文件 if (e.type == 'aborted') return //连接重置 可能是网络问题,而非 serverside 服务异常 //if (e.type != 'ECONNRESET') { //tasksMap[taskId].uploadId = '' //} if (!data.error.includes(taskIndex)) { data.error.push(taskIndex) tasksMap[taskId].error++ } tasksMap[taskId].message = e.message } if (abortSignal) return //计数 tasksMap[taskId].loaded += size tasksMap[taskId].currentLoaded = 0 // 如果retry 模式,则跳转到下一个error 部分 if (tasksMap[taskId].retry) { let errIdx = data.retried.indexOf(taskIndex) if (errIdx >= 0) { tasksMap[taskId].index = data.retried[errIdx + 1] || -1 data.retried.splice(errIdx, 1) } else { tasksMap[taskId].index = files.length } } else { tasksMap[taskId].index = taskIndex + 1 } delete data[key] setTimeout(next, 0) } tasksMap[taskId].status = STATUS.PROGRESS if (abortSignal) return next() } const remove = (taskId) => { if (tasksMap[taskId]) { try { // stopStream worker[taskId]?.cancel?.() //remove upload session const { index, destId } = tasksMap[taskId] const { data } = worker[taskId] let key = `${taskId}@${index}` console.log('clear session', destId, data[key]) driver?.clearSession?.(destId, data[key]) fs.rmSync(path.join(basePath, `${taskId}.json`), { force: false, recursive: true }) fs.rmSync(path.join(basePath, `${taskId}_data.json`), { force: false, recursive: true }) } catch (e) { } delete worker[taskId] delete tasksMap[taskId] } else { return { error: { message: 'task does not exist' } } } } /** * 暂停 * @param {*} taskId * @returns */ const pause = (taskId) => { if (!tasksMap[taskId]) { return { error: { message: 'task does not exist' } } } //初始化 和 移动状态时可用 if (tasksMap[taskId].status != STATUS.PROGRESS && tasksMap[taskId].status != STATUS.INIT) { throw { message: 'No need to pause in this state' } } try { // stopStream worker[taskId]?.cancel?.(true) } catch (e) { console.log(e) } tasksMap[taskId].status = STATUS.PAUSE } /** * 启动/恢复 * @param {*} taskId * @returns */ const resume = async (taskId) => { if (!tasksMap[taskId]) { return { error: { message: 'task does not exist' } } } if (tasksMap[taskId].status == STATUS.PAUSE) { //未读取完成 tasksMap[taskId].status = STATUS.INIT if (!tasksMap[taskId].inited) { createParseTask(taskId) } else { createTransferTask(taskId) } } } /** * 读取进度文件,并将所有任务置于暂停状态 */ const init = () => { for (let task of Object.values(tasksMap)) { if (task.status == STATUS.PROGRESS) { task.status = STATUS.PAUSE } let taskId = task.id let { data: files } = createDb(path.join(basePath, `${taskId}.json`), { autoSave: true }, []) tasksMap[taskId].currentDir = '' //任务错误文件 let { data } = createDb(path.join(basePath, `${taskId}_data.json`), { autoSave: true }, { error: [], retried: [] }) worker[taskId] = { files, data } } } const retry = (taskId) => { if (!tasksMap[taskId]) { throw { message: 'task does not exist' } } //重置files const { files, data } = worker[taskId] let successTotalSize = 0, successCount = 0 for (let i = 0; i < files.length; i++) { let file = files[i] if (!data.error.includes(i)) { successCount++ successTotalSize += file.size } } tasksMap[taskId].error = 0 tasksMap[taskId].status = STATUS.PROGRESS tasksMap[taskId].currentDir = '' tasksMap[taskId].loaded = successTotalSize tasksMap[taskId].success = successCount tasksMap[taskId].index = data.error[0] tasksMap[taskId].retry = 1 data.retried = [...data.error] data.error = [] createTransferTask(taskId) return {} } const get = (taskId) => { if (!tasksMap[taskId]) return { error: { message: 'task does not exist' } } try { // stopStream let { ...ret } = tasksMap[taskId] let files = worker[taskId].files ret.error = worker[taskId].data.error ret.files = files return ret } catch (e) { console.log(e) } } const getWorkers = () => { return [...Object.values(tasksMap)].reverse() } init() return { get, create, remove, pause, resume, all: getWorkers, retry } } ================================================ FILE: packages/sharelist/app/modules/core/theme.js ================================================ const fs = require('fs') const path = require('path') module.exports = (themeDir = []) => { let themes = [] const scanMeta = () => { let dirs = themeDir.filter(Boolean) let ret = {} for (let dir of dirs) { try { let files = fs.readdirSync(dir) for (let i of files) { let filepath = path.join(dir, i) let file = fs.statSync(filepath) if (file.isDirectory()) { let id = path.basename(i) ret[id] = filepath } } } catch (e) { console.log(e) } } themes = ret } const getFile = (file, theme) => { const themePath = themes[theme || 'default'] if (themePath) { return path.join(themePath, file) } else { return '' } } const get = (id) => { scanMeta() return id ? themes[id] : Object.keys(themes) } scanMeta() return { getFile, get } } ================================================ FILE: packages/sharelist/app/modules/core/upload.js ================================================ ================================================ FILE: packages/sharelist/app/modules/core/utils.js ================================================ const ignore = require('ignore') const crypto = require('crypto') const { pluginConfigKey } = require('./plugin') //过滤config 项目 const filterConfig = (config) => { let ret = {} for (let i of pluginConfigKey) { if (config[i]) ret[i] = config[i] } return ret } //TODO 过滤屏蔽路径 const isForbiddenPath = (p) => { return false } const isIgnorePath = (p = '', config) => { p = p.replace(/^\//, '') return p && ignore().add([].concat(config.acl_file, config.ignores)).ignores(p) } const isProxyPath = (p, config) => { p = p.replace(/^\//, '') return p && config.proxy_enable && ignore().add(config.proxy_paths).ignores(p) } exports.md5 = content => crypto.createHash('md5').update(content).digest("hex") exports.getFiles = async (driver, config, runtime) => { //使用路径模式,提前排除 if (runtime.path && isIgnorePath(runtime.path, config)) { return { error: { code: 404 } } } if (runtime.path && isForbiddenPath(runtime.path, config)) { return { error: { code: 404 } } } if (!config.index_enable) { return { error: { code: 403 } } } let data try { data = await driver.list(runtime) } catch (e) { console.trace(e) return { error: { ...e, code: e.code || 500, message: e.message } } } if (data.files?.length > 0) { let base_url = runtime.path == '/' ? '' : runtime.path data.files = data.files .filter(i => !isIgnorePath((base_url + '/' + i.name).substring(1), config) && i.hidden !== true ) if (!runtime.params.search) { data.files.forEach((i) => { //路径,相对于drive的绝对路径 // TODO 搜索结果 应在 web端忽略path 内容 i.path = i.extra?.path ? [runtime.driveName, i.extra?.path].join('/').replace(/\/{2,}/g, '/') : i.path if (i.config) { i.config = filterConfig(i.config) } }) } } if (data.config) { // console.log('SET data config', runtime.driveName) data.config = filterConfig(data.config) data.config.drive = runtime.driveName } return { data } } exports.getFile = async (driver, config, runtime) => { //使用路径模式,提前排除 if (runtime.path && isIgnorePath(runtime.path, config)) { return { error: { code: 404 } } } if (runtime.path && isForbiddenPath(runtime.path, config)) { return { error: { code: 404 } } } let data try { data = await driver.get(runtime) } catch (e) { return { error: { code: e.code || 500, msg: e.message } } } return { data } } exports.getPathById = async (driver, config, runtime) => { //使用路径模式,提前排除 if (runtime.path && isIgnorePath(runtime.path, config)) { return { error: { code: 404 } } } if (runtime.path && isForbiddenPath(runtime.path, config)) { return { error: { code: 404 } } } let data try { data = await driver.pwd(runtime.id) } catch (e) { return { error: { code: e.code || 500, msg: e.message } } } return { data } } exports.getContent = async (driver) => { } exports.getDownloadUrl = async (config, driver, runtime) => { //使用路径模式,提前排除 if (runtime.path && isIgnorePath(runtime.path, config)) { return { error: { code: 404 } } } if (runtime.path && isForbiddenPath(runtime.path, config)) { return { error: { code: 404 } } } try { let { url } = await driver.get_download_url(runtime) return { url } } catch (error) { return { error } } } exports.isUrl = (s) => { try { let parsed = new URL(s) return parsed.protocol == 'http:' || parsed.protocol == 'https:' } catch (err) { return false; } } ================================================ FILE: packages/sharelist/app/modules/guide/driver/aliyundrive.js ================================================ const { getOAuthAccessToken, PROXY_URL, render } = require('./shared') module.exports = async function (ctx, next) { render(ctx, `

挂载 Aliyun Drive

参考此链接

`) } ================================================ FILE: packages/sharelist/app/modules/guide/driver/baidu.js ================================================ const { PROXY_URL, render, btoa, atob } = require('./shared') const querystring = require('querystring') const getOAuthAccessToken = async (app, url, { client_id, client_secret, redirect_uri, code }, options = {}) => { let data = { client_id, client_secret, redirect_uri, code, grant_type: 'authorization_code' } let resp try { resp = await app.request(url, { method: 'get', data, ...options, }) } catch (e) { resp = { error: e?.message || e.toString() } } if (resp.data.error) { return { error: resp.data.error_description || resp.data.error } } return resp.data } module.exports = async function (ctx, next, app) { if (ctx.request.body && ctx.request.body.act && ctx.request.body.act == 'install') { let { client_id, client_secret } = ctx.request.body if (client_id && client_secret) { let baseUrl = ctx.origin + '/@guide/baidu/' + btoa([client_id, client_secret].join('::')) + '/callback' const opts = { client_id: client_id, scope: 'basic,netdisk', response_type: 'code', redirect_uri: PROXY_URL, state: baseUrl }; ctx.redirect(`https://openapi.baidu.com/oauth/2.0/authorize?${querystring.stringify(opts)}`) } } else if (ctx.params.pairs) { let [client_id, client_secret] = atob(ctx.params.pairs).split('::') if (ctx.query.code) { let credentials = await getOAuthAccessToken(app, 'https://openapi.baidu.com/oauth/2.0/token', { client_id, client_secret, code: ctx.query.code, redirect_uri: PROXY_URL }) console.log(credentials) if (credentials.error) { ctx.body = credentials.error } else { let ret = { AppKey: client_id, SecretKey: client_secret, redirect_uri: PROXY_URL, access_token: credentials.access_token, refresh_token: credentials.refresh_token } const cnt = Object.keys(ret).map(i => `
${i}:
${ret[i]}
`).join('
') render(ctx, `
${cnt}
`) } } else if (ctx.query.error) { ctx.body = req.query.error } } else { render(ctx, `

挂载Baidu Netdisk

1. 前往 Baidu网盘开发平台 注册应用获取 API KEY" 和 SECRET KEY,注册类别 请选择为【软件】。
2. 前往 应用详情->安全设置 将【OAuth授权回调页】设置为:

https://reruin.github.io/sharelist/redirect.html

,请遵循 使用协议
`) } } ================================================ FILE: packages/sharelist/app/modules/guide/driver/googledrive.js ================================================ const { PROXY_URL, render } = require('./shared') const getOAuthAccessToken = async (app, url, { client_id, client_secret, redirect_uri, code }, options = {}) => { let data = { client_id, client_secret, redirect_uri, code, grant_type: 'authorization_code' } let resp try { resp = await app.request.post(url, { data, contentType: 'json', ...options, }) } catch (e) { console.log(e) resp = { error: e?.message || e.toString() } } console.log(resp) if (resp.data.error) { return { error: resp.data.error_description || resp.data.error } } return resp.data } module.exports = async function (ctx, next, app) { if (ctx.request.body && ctx.request.body.act && ctx.request.body.act == 'install') { let { client_id, client_secret, redirect_uri, code } = ctx.request.body let credentials = await getOAuthAccessToken(app, 'https://oauth2.googleapis.com/token', { client_id, client_secret, code, redirect_uri }, { proxy: app.config.proxy_url }) if (credentials.error) { ctx.body = credentials.error } else { let ret = { client_id, client_secret, redirect_uri, refresh_token: credentials.refresh_token } let cnt = Object.keys(ret).map(i => `
${i}:
${ret[i]}
`).join('
') render(ctx, `
${cnt}
`) } } else if (ctx.params.pairs) { let [client_id, client_secret] = app.utils.atob(ctx.params.pairs).split('::') if (ctx.query.code) { let credentials = await getOAuthAccessToken(app, 'https://oauth2.googleapis.com/token', { client_id, client_secret, code: ctx.query.code, redirect_uri: PROXY_URL }, { proxy: app.config.proxy_url }) if (credentials.error) { ctx.body = credentials.error } else { let cnt = Object.keys(credentials).map(i => `
${i}:
${credentials[i]}
`).join('
') render(ctx, `
${cnt}
`) } } else if (ctx.query.error) { ctx.body = req.query.error } } else { render(ctx, `

挂载GoogleDrive

1. 请参考 此链接创建项目,获取 Client ID / Client Secret。你也可以使用 rclone 的这组,但是有一定几率触发 Rate Limit Exceeded

Client ID:202264815644.apps.googleusercontent.com

Client Secret:X4Z3ca8xfWDb1Voo-F9a7ZxJ

2. 在下方填写Client ID / Client Secret后,点击获取验证code,若出现[Google hasn't verified this app],请展开Advanced,点击[Go to Quickstart (unsafe)]。

`) } } ================================================ FILE: packages/sharelist/app/modules/guide/driver/onedrive.js ================================================ const { PROXY_URL, render, btoa, atob } = require('./shared') const querystring = require('querystring') const support_zone = { 'GLOBAL': [ 'https://login.microsoftonline.com', 'https://graph.microsoft.com', 'https://portal.azure.com', '全球', 'https://www.office.com/', ['00cdbfd5-15a5-422f-a7d7-75e8eddd8fa8', 'pTvE-.ooe8ou5p1552O8s.3WK996UZ.Z8M'], ], 'CN': [ 'https://login.chinacloudapi.cn', 'https://microsoftgraph.chinacloudapi.cn', 'https://portal.azure.cn', '世纪互联', 'https://portal.partner.microsoftonline.cn/Home', ['9430c343-440f-44f3-ba1d-18b77c0072af', '8f3.2dD-_.6mLv-VmMo6vCxuYcm5~Liqn4'], ], 'DE': [ 'https://login.microsoftonline.de', 'https://graph.microsoft.de', 'https://portal.microsoftazure.de', 'Azure Germany' ], 'US': [ 'https://login.microsoftonline.us', 'https://graph.microsoft.us', 'https://portal.azure.us', 'Azure US GOV', ] } const getAuthority = (zone, tenant_id) => { return support_zone[zone || 'COMMON'][0] + '/' + (tenant_id || 'common') } const getGraphEndpointSite = (zone, site_name) => { return support_zone[zone || 'COMMON'][1] + '/v1.0/sites/root:/' + site_name } const getDefaultConfig = (zone) => { return support_zone[zone || 'COMMON'][5] || [] } const getAccessToken = async (app, data) => { let { zone, site_name, ...formdata } = data let metadata = getAuthority(zone) formdata.redirect_uri = PROXY_URL formdata.grant_type = 'authorization_code' let res try { let resp = await app.request.post(`${metadata}/oauth2/v2.0/token`, { data: formdata, contentType: 'form' }) res = resp.data } catch (e) { res = { error: e.toString() } } console.log(res) if (res.error) { return { error: `[${res.error}]${res.error_description}` } } let ret = { ...res } // get sharepoint site id if (site_name) { let api = getGraphEndpointSite(zone, site_name) try { let resp = await app.curl(api, { headers: { 'Authorization': 'Bearer ' + ret.access_token } }) ret.site_id = resp.body.id } catch (e) { return { error: 'parse site id error' } } } return ret } module.exports = async function (ctx, next, app) { if (ctx.request.body && ctx.request.body.act && ctx.request.body.act == 'install') { let { client_id, client_secret, zone, tenant_id = 'common', custom, sharepoint_site, type } = ctx.request.body let site_name, err if (custom) { if (!client_id || !client_secret) { err = 'require client_id and client_secret' } } else { [client_id, client_secret] = getDefaultConfig(zone) if (!client_id) { err = '暂不支持当前地域' } } if (type == 'sharepoint') { if (sharepoint_site) { let obj = new URL(sharepoint_site) site_name = obj.pathname } else { err = '请填写sharepoint站点URL
require sharepoint site' } } if (err) { render(ctx, `
'

挂载向导

'

${err}

点击重新开始

`) } else if (client_id && client_secret && zone) { let site_name = '' if (sharepoint_site) { let obj = new URL(sharepoint_site) site_name = obj.pathname } let baseUrl = ctx.origin + '/@guide/onedrive/' + btoa([client_id, client_secret, zone, site_name].join('::')) + '/callback' const opts = { client_id: client_id, scope: [ 'offline_access', 'files.readwrite.all' ].join(' '), response_type: 'code', redirect_uri: PROXY_URL, state: baseUrl }; ctx.redirect(`${support_zone[zone][0] + '/' + (tenant_id || 'common')}/oauth2/v2.0/authorize?${querystring.stringify(opts)}`) } } // 挂载验证回调 else if (ctx.params.pairs) { let [client_id, client_secret, zone, site_name] = atob(ctx.params.pairs).split('::') if (ctx.query.code) { let credentials = await getAccessToken(app, { client_id, client_secret, code: ctx.query.code, zone, site_name }) if (credentials.error) { ctx.body = credentials.error } else { let ret = { client_id, client_secret, redirect_uri: PROXY_URL, refresh_token: credentials.refresh_token } console.log(ret) if (site_name) { let api = getGraphEndpointSite(zone, site_name) try { let resp = await app.request(api, { headers: { 'Authorization': 'Bearer ' + credentials.access_token } }) ret.site_id = resp.data.id } catch (e) { ctx.body = 'parse site id error' return } } let cnt = Object.keys(ret).map(i => `
${i}:
${ret[i]}
`).join('
') render(ctx, `
${cnt}
`) } } else if (ctx.query.error) { ctx.body = req.query.error } } else { let zone = [], types = ['onedrive', 'sharepoint'] for (let [key, value] of Object.entries(support_zone)) { zone.push(``) } render(ctx, `

OneDrive 挂载向导

前往 office365,点击应用 sharepoint,创建一个网站,将URL地址填入下方输入框中。

前往 Azure管理后台 注册应用获取 应用ID 和 应用机密。重定向 URI 请设置为:

https://reruin.github.io/sharelist/redirect.html

`) } } ================================================ FILE: packages/sharelist/app/modules/guide/driver/shared.js ================================================ exports.PROXY_URL = 'https://reruin.github.io/sharelist/redirect.html' exports.render = (ctx, cnt) => { return ctx.body = `ShareList ${cnt}` } exports.btoa = v => Buffer.from(v).toString('base64') exports.atob = v => Buffer.from(v, 'base64').toString() ================================================ FILE: packages/sharelist/app/modules/guide/index.js ================================================ const baidu = require('./driver/baidu') const onedrive = require('./driver/onedrive') const googledrive = require('./driver/googledrive') const aliyundrive = require('./driver/aliyundrive') module.exports = (inject) => { const vendor = { onedrive, googledrive, baidu, aliyundrive } return { config() { let guide = {} for (let i in vendor) { guide[i] = `/@guide/${i}` } return { guide } }, route: [ { method: 'all', path: '/@guide/:type', flush: 'pre', handler: async (ctx, next) => { await vendor[ctx.params.type]?.(ctx, next, inject) } }, { method: 'get', path: '/@guide/:type/:pairs(.*)/callback', flush: 'pre', handler: async (ctx, next) => { await vendor[ctx.params.type]?.(ctx, next, inject) } } ] } } ================================================ FILE: packages/sharelist/app/modules/server/controller.js ================================================ const fs = require('fs') const { nanoid } = require('nanoid') const path = require('path') const { createRuntime, selectSource, send, sendfile, uploadManage, emptyStream, createUpdateManage } = require('./runtime') module.exports = (sharelist, appConfig) => { const getConfig = async (raw = false) => { if (raw) return sharelist.config let config = { ...sharelist.config } // if (config.drives) { // config.drives = sharelist.getDisk() // } config.drivers = sharelist.driver.getDriver().filter(i => i.mountable !== false) config.theme_options = sharelist.theme.get() config.plugin_source_options = sharelist.plugin.getSources() config.plugins = sharelist.plugin.get() config.guide = appConfig.guide config.pluginConfig = sharelist.driver.getPluginConfig() return config } const getCustomConfig = () => { const ret = {} const defaultConfigKey = sharelist.defaultConfigKey const config = { ...sharelist.config } for (let i of Object.keys(config)) { if (!defaultConfigKey.includes(i)) { ret[i] = config[i] } } ret.version = appConfig.version return ret } const getManageFile = (file) => { if (appConfig.manageDir) { return path.join(appConfig.manageDir, file) } else { return '' } } const get = async (runtime) => { let { data, error } = await sharelist.file(runtime) if (error) { return { error } } if (data.type == 'file') { if (runtime.params.download) { if (runtime.params.preview) { if (data.extra.preview_url) { return await send(sharelist, { ...data, download_url: data.extra.preview_url, reqHeaders: runtime.headers }) } else if (data.extra.sources) { let download_url = selectSource(data.extra.sources, runtime.params.preview) || data.download_url return await send(sharelist, { ...data, download_url, reqHeaders: runtime.headers }) } else { console.log('here') return await send(sharelist, { ...data, reqHeaders: runtime.headers }) } } else { if (sharelist.config.anonymous_download_enable) { return await send(sharelist, { ...data, reqHeaders: runtime.headers }) } } } else { if (!data.download_url || data.extra?.proxy) { data.download_url = '/api/drive/get?download=true&id=' + encodeURIComponent(data.id) } return data } } else { if (!data.download_url) { data.download_url = '/api/drive/get?download=true&id=' + encodeURIComponent(data.id) } return data } return { status: 404 } } const list = async (runtime) => { let st = Date.now() let { data, error } = await sharelist.files(runtime) if (error) { return { error } } else { if (data.files?.length > 0) { data.files // .sort((a, b) => (a.type == 'folder' ? -1 : 1)) .forEach((i) => { if (i.type == 'file') { let download_url = '/api/drive/file/get?download=true&id=' + encodeURIComponent(i.id) i.download_url = download_url if (i.extra?.preview_url) { i.preview_url = download_url + '&preview=true' } if (i.extra?.sources) { i.sources = i.extra.sources.map(i => ({ quality: i.quality, src: download_url + '&preview=' + i.quality })) } } }) } return data } } const manageReplacer = (data) => { const basePath = sharelist.config.manage_path return data.replace('', ``).replace('src="./', `src="${basePath}/`).replace('href="./', `href="${basePath}/`) } return { async page(ctx, next) { //let filepath = getFilePath(ctx.path == '/' ? 'index.html' : ctx.path.substring(1), this.app) const { getThemeFile, config } = sharelist const managePath = config.manage_path || '/@manage' if (managePath && ctx.path.startsWith(managePath)) { let filepath = ctx.path.replace(managePath, '') if (filepath == '' && !ctx.path.endsWith('/')) { ctx.redirect(ctx.path + '/') return } filepath = getManageFile((filepath == '/' || filepath == '') ? 'index.html' : filepath) let isFileExist = false try { if (fs.existsSync(filepath)) { isFileExist = true } } catch (e) { } if (!isFileExist) { filepath = getManageFile('index.html') } let replacer = filepath.endsWith('index.html') ? manageReplacer : null return await sendfile(ctx, filepath, replacer) } let filepath = getThemeFile(ctx.path == '/' ? 'index.html' : ctx.path.substring(1)) // if url is '/filename_path?download' if ('download' in ctx.query) { await get(ctx, next) } else { try { if (!fs.existsSync(filepath)) { filepath = getThemeFile('index.html') } } catch (e) { filepath = getThemeFile('index.html') } console.log(Date.now()) let status = await sendfile(ctx, filepath) if (!status) { console.log(Date.now()) let r = await this.list(ctx, next) console.log(Date.now()) return r } } }, async setting(ctx, next) { ctx.body = { data: await getConfig(!!ctx.query.raw) } }, async userConfig(ctx, next) { const data = getCustomConfig() ctx.body = { status: 0, data } }, async configField(ctx, next) { const data = getCustomConfig() const key = ctx.query.key || ctx.params.field const ret = key && data[key] ? data[key] : '' if (ctx.query['content-type']) { ctx.set('content-type', ctx.query['content-type']) ctx.body = ret } else { ctx.body = { status: 0, data: ret } } }, async reload(ctx, next) { await sharelist.reload() ctx.body = { status: 0 } }, async reloadBench(ctx) { for (let i = 0; i < 3000; i++) { await sharelist.reload() } ctx.body = { status: 0 } }, async updateSetting(ctx, next) { let data = { ...ctx.request.body } for (let i in data) { let val = data[i] if (i == 'drives') { sharelist.setDrives(val) } else { sharelist.config[i] = val } } ctx.body = { data: await getConfig() } }, async getPlugin(ctx, next) { const id = ctx.params.id const ret = sharelist.plugin.get(id) if (ret && ret.path) { ctx.body = { status: 0, data: fs.readFileSync(ret.path, 'utf-8') } } else { ctx.body = { status: 0, data: '' } } }, async setPlugin(ctx, next) { const { id, data } = ctx.request.body try { await sharelist.plugin.set(id, data) ctx.body = { status: 0 } } catch (e) { ctx.body = { status: -1, msg: e.message || '保存失败' } } }, async removePlugin(ctx) { const id = ctx.params.id //try { await sharelist.plugin.remove(id) ctx.body = {} //} catch (e) { ctx.body = {} //} }, async upgradePlugin(ctx) { const id = ctx.params.id //try { await sharelist.plugin.upgrade(id) ctx.body = {} //} catch (e) { // ctx.body = { status: -1, msg: e.message || '更新失败' } //} }, async clearCache(ctx, next) { sharelist.cache.clear() ctx.body = { status: 0 } }, async getPath(ctx, next) { // let { id } = ctx.request.body let runtime = await createRuntime(ctx) let { data, error } = await sharelist.getPathById(runtime) if (error) { ctx.body = { error } return } console.log('getPath', data) ctx.body = data }, async get(ctx, next) { let runtime = await createRuntime(ctx) let data = await get(runtime) if (data.status) { ctx.status = data.status if (data.headers) { ctx.set(data.headers) } if (data.body) { ctx.body = data.body } } else if (data.redirect) { ctx.redirect(data.redirect) } else { ctx.body = data } }, async list(ctx, next) { let runtime = await createRuntime(ctx) ctx.body = await list(runtime) }, //upload request 有滞后性,需要接口手动停止 async cancelUpload(ctx) { uploadManage.remove(ctx.params.id) ctx.body = {} }, //查询/创建上传 ,即使后端服务重启后 查询依旧有效 async createUpload(ctx) { let { task_id, ...rest } = ctx.request.body if (task_id && uploadManage.getTask(task_id)) { rest = uploadManage.getTask(task_id) } let { size, hash, hash_type, id, name, state, dest } = rest let options = { size, name, state } if (hash_type && hash) { options.hash = hash options.hash_type = hash_type } if (dest) { let dests = dest.split('/') let parent = await sharelist.driver.mkdir(id, dests, {}, true) if (parent?.id) id = parent.id } let res = await sharelist.driver.upload(id, null, options) if (res.completed) { ctx.body = { completed: true } } else { let taskId = uploadManage.createTask({ id, name, size, hash, hash_type, state: res.state, dest }) ctx.body = { taskId, start: res.start, completed: res.completed } } }, async upload(ctx) { let { task_id, ...rest } = ctx.query if (task_id && uploadManage.getTask(task_id)) { rest = uploadManage.getTask(task_id) } let { size, hash, hash_type, id, name, upload_id } = rest let stream = ctx.req let options = { size, name, uploadId: upload_id } if (hash_type && hash) { options[hash_type] = hash.content options.hash = hash options.hash_type = hash_type } let controller = new AbortController() options.signal = controller.signal if (upload_id) { uploadManage.add({ req: ctx.req, controller, taskId: task_id }, upload_id) } options.setUploadState = (extraData) => { uploadManage.updateTask(task_id, { extraData }) } stream.pause() let res = await sharelist.driver.upload(id, stream, options) ctx.body = res }, async pluginStore(ctx) { ctx.body = await sharelist.plugin.getFromStore() }, async installPlugin(ctx) { let { url } = ctx.request.body try { await sharelist.plugin.createFromUrl(url) ctx.body = { status: 0 } } catch (e) { ctx.body = { error: { message: e.message || '安装失败' } } } }, async tasks(ctx) { let tasks = await sharelist.transfer.all() ctx.body = tasks }, async transfer(ctx) { let id = ctx.params.id ctx.body = await sharelist.transfer.get(id) }, async removeTransfer(ctx) { let id = ctx.params.id ctx.body = await sharelist.transfer.remove(id) }, async resumeTransfer(ctx) { let id = ctx.params.id ctx.body = await sharelist.transfer.resume(id) }, async pauseTransfer(ctx) { let id = ctx.params.id ctx.body = await sharelist.transfer.pause(id) }, async retryTransfer(ctx) { let id = ctx.params.id ctx.body = await sharelist.transfer.retry(id) }, async remove(ctx) { let { id } = ctx.request.body if (id) { await sharelist.driver.rm(id) ctx.body = { id } } }, async mkdir(ctx) { let { id, name } = ctx.request.body if (id) { let data = await sharelist.driver.list({ id }) if (data.files && data.files.includes(i => i.name == name)) { ctx.body = { error: { message: '此目录下已存在同名文件,请修改名称' } } } else { let res = await sharelist.driver.mkdir(id, name) if (res.id) { ctx.body = { id: res.id, name, type: 'folder' } } } } }, async hashSave(ctx) { let { id, hash, name, size } = ctx.request.body let res = await sharelist.driver.hashUpload(id, { hash, name, size }) if (res) { res.name = name res.type = 'file' } ctx.body = res }, async update(ctx) { let { id, name, dest, mode, threadNum } = ctx.request.body if (name) { let res = await sharelist.driver.rename(id, name) ctx.body = { name: res.name } } // move / copy / transfer else if (dest) { let isSameDrive = await sharelist.driver.isSameDrive(id, dest) if (isSameDrive) { let res = await sharelist.driver.mv(id, dest, mode == 'copy') ctx.body = {} } else { await sharelist.transfer.create(id, dest, true, { threadNum }) ctx.body = {} } } }, async removeDisk(ctx) { let { disks } = ctx.request.body } } } ================================================ FILE: packages/sharelist/app/modules/server/index.js ================================================ const Koa = require('koa') const koaCors = require('@koa/cors') const koaBody = require('koa-body') const koaJson = require('koa-json') const createRouter = require('./router') const createApi = require('./controller') const pkg = require('../../../package.json') class Server { constructor(sharelist, appConfig) { this.modules = [] this.sharelist = sharelist this.appConfig = appConfig const app = new Koa() app.use(koaCors()) app.use(koaBody()) app.use(koaJson()) app.use(async (ctx, next) => { try { await next() } catch (error) { console.log(error) if (error instanceof Error) { ctx.body = { error: { message: error.message } } } else { ctx.body = { error } } } }) this.app = app } use(module) { this.modules.push(module) } createConfig() { let configs = this.modules.map(i => i.config).filter(Boolean) let mergeConfig = { version: pkg.version, ...this.appConfig } for (let config of configs) { if (typeof config == 'function') { config = config(mergeConfig) } Object.assign(mergeConfig, config) } return mergeConfig } start() { createRouter(this.app, this.sharelist, createApi(this.sharelist, this.createConfig()), this.modules.map(i => i.route).filter(Boolean).flat()) } } function createServer(...args) { return new Server(...args) } module.exports = createServer ================================================ FILE: packages/sharelist/app/modules/server/router.js ================================================ const Router = require('@koa/router') const createAuth = (sharelist) => async (ctx, next) => { let token = ctx.get('authorization') || ctx.query.token let isAdmin = sharelist.checkAccess(token) if (isAdmin) { await next() } else { ctx.body = { error: { code: 401, message: 'Invalid password' } } } } module.exports = (app, sharelist, api, mergeRoutes) => { const auth = createAuth(sharelist) const router = new Router() mergeRoutes.filter(i => i.flush == 'pre').forEach(i => { router[i.method](i.path, i.handler) }) router .get('/api', (ctx) => { ctx.body = 'hello!' }) .get('/api/setting', auth, api.setting) .get('/api/user_config', api.userConfig) .post('/api/setting', auth, api.updateSetting) .put('/api/cache/clear', auth, api.clearCache) .put('/api/reload', auth, api.reload) .get('/api/reloadBench', api.reloadBench) .get('/api/drive/file/get', api.get) .post('/api/drive/file/get', api.get) .post('/api/drive/file/list', api.list) .post('/api/drive/file/delete', api.remove) .post('/api/drive/file/update', api.update) .post('/api/drive/file/mkdir', api.mkdir) .post('/api/drive/file/upload', api.upload) .post('/api/drive/file/create_upload', api.createUpload) .post('/api/drive/file/hash_save', api.hashSave) .get('/api/drive/file/cancel_upload/:id', api.cancelUpload) .post('/api/drive/file/path', api.getPath) .post('/api/drive/disk/delete', api.removeDisk) .get('/api/drive/tasks', api.tasks) .get('/api/drive/task/transfer/:id', api.transfer) .delete('/api/drive/task/transfer/:id', api.removeTransfer) .put('/api/drive/task/transfer/:id/resume', api.resumeTransfer) .put('/api/drive/task/transfer/:id/pause', api.pauseTransfer) .put('/api/drive/task/transfer/:id/retry', api.retryTransfer) // .post('/api/drive/task/remote_download', api.remoteDownload) // .put('/api/drive/task/remote_download/:id/pause', api.remoteDownloadPause) // .put('/api/drive/task/remote_download/:id/resume', api.remoteDownloadResume) // .delete('/api/drive/task/remote_download/:id', api.remoteDownloadRemove) .get('/api/config/:field', api.configField) .post('/api/plugin_store', auth, api.pluginStore) .post('/api/plugin_store/install', auth, api.installPlugin) .put('/api/plugin/:id(.*)/upgrade', auth, api.upgradePlugin) .get('/api/plugin/:id(.*)', auth, api.getPlugin) .post('/api/plugin', auth, api.setPlugin) .delete('/api/plugin/:id(.*)', auth, api.removePlugin) .get('/api/drive/path', api.list) .get('/api/drive/path/:path(.*)', api.list) .get('/api/drive/:path\\:file', api.get) .get('/api/transfer', api.transfer) .get('/:path(.*)', api.page) mergeRoutes.filter(i => i.flush == 'post').forEach(i => { router[i.method](i.path, i.handler) }) app .use(router.routes()) .use(router.allowedMethods()); return router } ================================================ FILE: packages/sharelist/app/modules/server/runtime.js ================================================ const { URLSearchParams } = require('url') const promisify = require('util').promisify const extname = require('path').extname const fs = require('fs') const calculate = require('etag') const stat = promisify(fs.stat) const mime = require('mime') const { Readable } = require('stream') const { nanoid } = require('nanoid') const parseQuery = (str) => { let params = new URLSearchParams(str) let ret = {} if (params.has('forward')) { ret.forward = true } if (params.has('download')) { ret.download = true } if (params.has('preview')) { ret.preview = params.get('preview') } if (params.has('order_by')) { let s = params.get('order_by') let r = {} for (let i of s.split('+')) { let pairs = i.split(':') if (pairs.length == 2) { r[pairs[0]] = pairs[1] } } ret.order_by = r } if (params.has('auth')) { ret.auth = params.get('auth') } if (params.has('search')) { ret.search = decodeURIComponent(params.get('search')) } return ret } const mergeHeaders = (a, b) => { const exclude = ['host', 'accept-encoding'] let pre = { ...a, ...b } let headers = {} for (let key in pre) { if (exclude.includes(key) == false) { headers[key] = pre[key] } } return headers } const getRange = (r, total) => { if (r) { let [, start, end] = r.match(/(\d*)-(\d*)/); start = start ? parseInt(start) : 0 end = end ? parseInt(end) : total - 1 return { start, end } } } const createHeaders = (stats, { maxage, immutable, range } = { maxage: 0, immutable: false }) => { let fileSize = stats.size let fileName = stats.name let headers = {} headers['Last-Modified'] = new Date(stats.mtime).toUTCString() headers['Content-Type'] = mime.getType(fileName) if (range) { let { start, end } = range headers['Content-Range'] = `bytes ${start}-${end}/${fileSize}` headers['Content-Length'] = end - start + 1 headers['Accept-Ranges'] = 'bytes' } else { header['Content-Range'] = `bytes 0-${fileSize - 1}/${fileSize}` headers['Content-Length'] = fileSize } headers['Content-Disposition'] = `attachment;filename=${encodeURIComponent(fileName)}` return headers } const parseSort = (order_by) => { let cats = ['name', 'size', 'ctime', 'mtime'] if (order_by) { let [cat, type = 'asc'] = order_by.toLowerCase().split(' ') if (cats.includes(cat)) { return [cat, type == 'asc' ? 1 : 0] } } } const parsePathAndDrive = (runtime, path) => { runtime.paths = (path || '').replace(/\/$/, '').split('/').filter(Boolean).map(decodeURIComponent) runtime.path = '/' + runtime.paths.join('/') runtime.driveName = runtime.paths[0] } exports.createRuntime = async (ctx) => { let token = ctx.get('authorization') || ctx.query.token let runtime = { method: ctx.method, token, headers: ctx.headers } let { id, path, ...others } = ctx.method == 'POST' ? ctx.request.body : ctx.query if (id) { runtime.id = id } else { parsePathAndDrive(runtime, path) } let { order_by, next_page, ...options } = ctx.method == 'POST' ? others : parseQuery(ctx.querystring) if (order_by) { options.orderBy = parseSort(order_by) } if (next_page) { options.nextPage = next_page } runtime.params = options return runtime } exports.selectSource = (sources, quality = 'HD') => { let map = {} sources.forEach(i => { map[i.quality] = i.src }) return map[quality] || map['HD'] || map['SD'] || map['LD'] } const notfound = { ENOENT: true, ENAMETOOLONG: true, ENOTDIR: true } exports.sendfile = async (ctx, path, replacer) => { try { const stats = await stat(path) if (!stats) return null if (!stats.isFile()) return stats ctx.response.status = 200 ctx.response.lastModified = stats.mtime ctx.response.type = mime.getType(path) if (!replacer) { ctx.response.length = stats.size if (!ctx.response.etag) { ctx.response.etag = calculate(stats, { weak: true }) } } console.log(ctx.request.method, path, ctx.request.fresh, !replacer) // fresh based solely on last-modified switch (ctx.request.method) { case 'HEAD': ctx.status = ctx.request.fresh && !replacer ? 304 : 200 break case 'GET': if (ctx.request.fresh && !replacer) { ctx.status = 304 } else { if (replacer) { ctx.body = replacer(fs.readFileSync(path, 'utf-8')) } else { ctx.body = fs.createReadStream(path) } } break } return stats } catch (err) { if (notfound[err.code]) return err.status = 500 throw err } } /** * send sharelist data * @param {ctx} ctx * @param {object} sharelist * @param {object} data */ exports.send = async (sharelist, data) => { let { download_url } = data let isGlobalProxy = !!sharelist.config.proxy_enable if (download_url) { //the request need proxy if (data.extra?.proxy || isGlobalProxy) { let reqHeaders = mergeHeaders(data.reqHeaders, data.extra?.proxy?.headers || {}) let options = { headers: reqHeaders, responseType: 'stream' } if (data.extra?.proxy_server) { options.proxy = data.extra?.proxy_server } let { data: stream, status, error, headers } = await sharelist.request(download_url, options) // compatible if (headers['accept-ranges'] == 'bytes' && headers['content-range']) { status = 206 } if (error) { return { status: 500 } } else { return { headers, status, body: stream } } } else { return { redirect: download_url } } } else { let range = getRange(data.reqHeaders.range, data.size) || { start: 0, end: data.size - 1 } let { stream, error, status, headers, enableRanges = false } = await sharelist.driver.createReadStream(data.id, range) let isReqRange = !!data.reqHeaders.range if (stream) { let options = enableRanges ? { range } : {} return { headers: headers || createHeaders(data, options), status: status || (isReqRange && enableRanges ? 206 : 200), body: stream } } else { return { status: 404, body: error?.message || `can't find stream` } } } } // 暂停 : destroy() => close. 完成 end => close. 客户端异常 error => close const createUploadManage = () => { let tasks = {} let tasksMetaMap = {} const remove = (id) => { if (id && tasks[id]) { tasks[id].req.destroy(new Error('AbortError')) } } const add = (data, id) => { if (id && tasks[id]) { remove(id) } tasks[id] = data data.req.once('error', () => { data.controller.abort() }) data.req.once('close', () => { if (tasksMetaMap[data.taskId]) { delete tasksMetaMap[data.taskId] } delete tasks[id] }) } //临时上传链 const createTask = (data) => { let taskId = nanoid() tasksMetaMap[taskId] = data return taskId } const getTask = (taskId) => tasksMetaMap[taskId] const updateTask = (taskId, data) => { let src = tasksMetaMap[taskId] if (src) { for (let i in data) { src[i] = data[i] } } } return { remove, add, createTask, getTask, updateTask } } const createUpdateManage = (sharelist) => { let tasks = {} const remove = (id) => { if (id && tasks[id]) { tasks[id].req.destroy(new Error('AbortError')) } } const add = (data, id) => { if (id && tasks[id]) { remove(id) } tasks[id] = data data.req.once('error', () => { data.controller.abort() }) data.req.once('close', () => { delete tasks[id] }) } const get = () => { } return { remove, add, get } } exports.uploadManage = createUploadManage() exports.createUpdateManage = createUpdateManage exports.emptyStream = () => { const controller = new AbortController(); const stream = new Readable({ read(size) { this.destroy() }, signal: controller.signal }) stream.pause() return { stream, controller } } ================================================ FILE: packages/sharelist/app/modules/webdav/index.js ================================================ const { WebDAVServer } = require('@sharelist/webdav') const isWebDAVRequest = (ctx, webdavPath) => { if (webdavPath == '/') { return /(Microsoft\-WebDAV|FileExplorer|WinSCP|WebDAVLib|WebDAVFS|rclone|Kodi|davfs2|sharelist\-webdav|RaiDrive|nPlayer|LibVLC|PotPlayer|gvfs)/i.test(ctx.request.headers['user-agent']) || ('translate' in ctx.request.headers) || ('overwrite' in ctx.request.headers) || ('depth' in ctx.request.headers) } else { return ctx.params.path.startsWith(webdavPath) } } module.exports = (sharelist) => { const { config } = sharelist const webdavPath = config.webdav_path || '/' const webdavServer = new WebDAVServer({ driver: sharelist.driver.createAction({ useProxy: () => !!config.webdav_proxy, baseUrl: webdavPath }), base: webdavPath, auth: (user, pass) => { return !config.webdav_pass || (config.webdav_user === user && config.webdav_pass === pass) } }) return { route: [{ method: 'all', path: ':path(.*)', flush: 'pre', handler: async (ctx, next) => { let webdavPath = config.webdav_path || '/' if (!isWebDAVRequest(ctx, webdavPath)) { await next() return } console.log('[WebDAV]', ctx.method, ctx.url, '<-->', ctx.ip) // console.log(ctx.headers) // if (ctx.method == 'PROPPATCH') console.log(ctx.headers) let res try { res = await webdavServer.request(ctx.req, { base: webdavPath }) } catch (e) { console.log(e) res = { status: e?.code || 500 } } const { headers, status, body } = res if (status == 302) { ctx.redirect(body) } else { if (headers) { ctx.set(headers) } if (status) { ctx.status = parseInt(status) } if (body) { // ctx.set('Content-Length', body.length) ctx.body = body } } } }] } } ================================================ FILE: packages/sharelist/app/shared/send.js ================================================ ================================================ FILE: packages/sharelist/app.js ================================================ #!/usr/bin/env node /** * Module dependencies. */ const bootstrap = require('./app/main') const http = require('http') const os = require('os') const fs = require('fs') const onError = (error) => { if (error.syscall !== 'listen') { throw error } // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error('Pipe requires elevated privileges') process.exit(1) break case 'EADDRINUSE': console.error('Port is already in use') process.exit(1) break default: throw error } } const getIpv4 = () => { var ifaces = os.networkInterfaces() for (var dev in ifaces) { for (var i in ifaces[dev]) { var details = ifaces[dev][i] if (/^\d+\./.test(details.address)) { return details.address } } } } bootstrap().then(({ app, port }) => { const server = http.createServer(app.callback()) server.on('error', onError).on('listening', () => { console.log(`[${new Date().toISOString()}] Sharelist Server is running at http://` + getIpv4() + ':' + server.address().port + '/') }) server.listen(process.env.PORT || port || 33001) }) ================================================ FILE: packages/sharelist/docker-compose.yml ================================================ version: "3" services: sharelist: image: reruin/sharelist volumes: - $HOME/sharelist:/sharelist/cache ports: - "33001:33001" ================================================ FILE: packages/sharelist/package.json ================================================ { "name": "sharelist", "version": "0.4.4", "bin": "app.js", "repository": "https://github.com/reruin/sharelist", "license": "MIT", "scripts": { "start": "node app.js", "dev": "cross-env NODE_ENV=dev nodemon app.js -i ./cache", "test": "echo \"Error: no test specified\" && exit 1", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .", "pkg": "pkg . --output build/sharelist --targets linux-x64,linux-arm64,linuxstatic-armv7,macos-x64,win-x64 --public-packages '*'", "pkg-local": "pkg . --output build/sharelist --targets node16-win-x64 --compress", "pkg-linux": "pkg . --output build/sharelist --targets node16-linux-x64", "release": "node ../../scripts/release.js --skipBuild --skipNpmPublish" }, "dependencies": { "@koa/cors": "^3.1.0", "@koa/router": "^12.0.0", "@sharelist/core": "^0.2", "@sharelist/webdav": "^0.2", "@vue-reactivity/watch": "^0.2.0", "@vue/reactivity": "^3.2.33", "bonjour": "^3.5.0", "etag": "^1.8.1", "global": "^4.4.0", "html-entities": "^2.3.3", "ignore": "^5.1.8", "koa": "^2.13.1", "koa-body": "^4.2.0", "koa-json": "^2.0.2", "koa-logger": "^3.2.1", "koa-onerror": "^4.1.0", "koa-router": "^10.0.0", "koa-sendfile": "^3.0.0", "koa-session-minimal": "^4.0.0", "koa-static-cache": "^5.1.4", "markdown-it": "^12.0.6", "mime": "^2.5.2", "nanoid": "^3.1.23", "node-fetch": "^2.6.1", "node-rsa": "^1.1.1", "semver-compare-lite": "^0.1.0", "write-file-atomic": "^3.0.3" }, "pkg": { "scripts": [], "assets": [ "./web/**/*", "./manage/**/*", "./plugins.json" ] } } ================================================ FILE: packages/sharelist-core/.prettierrc.js ================================================ // https://prettier.io/docs/en/configuration.html module.exports = { //分号终止符 semi: false, //行尾逗号 trailingComma: "all", // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号) singleQuote: true, printWidth: 120, // 换行符 endOfLine: "auto", //缩进 default:2 tabWidth: 2 } ================================================ FILE: packages/sharelist-core/CHANGELOG.md ================================================ ## [0.1.7](https://github.com/reruin/sharelist/compare/v0.3.6...v0.1.7) (2021-10-14) ### Bug Fixes * **core:** fix some bugs ([c8ee9e6](https://github.com/reruin/sharelist/commit/c8ee9e655687d49420c54c9331a91b19aae10beb)) ## [0.1.6](https://github.com/reruin/sharelist/compare/v0.3.5...v0.1.6) (2021-10-12) ## [0.1.5](https://github.com/reruin/sharelist/compare/v0.3.3...v0.1.5) (2021-10-10) ### Bug Fixes * **core:** add createReadStream ([27350fb](https://github.com/reruin/sharelist/commit/27350fb6a036ab13e65dc0b52dab3f85732d5667)) ================================================ FILE: packages/sharelist-core/LICENSE ================================================ MIT License Copyright (c) 2021-present, Reruin and Sharelist contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/sharelist-core/README.md ================================================ # @sharelist/core [![npm](https://img.shields.io/npm/v/@sharelist/core.svg)](https://npmjs.com/package/@sharelist/core) It's a framework for mounting netdisk. ## Useage ```js const sharelist = require('@sharelist/core') const driver = sharelistCore({ config, plugins: [/* some sharelist plugins */]}) // And you can use this driver const disk = await driver.list() // find dir const parentDir = disk.files.find(i => i.type == 'folder') // mkdir const newDir = await driver.mkdir(parentDir.id) // rename await driver.rename(newDir.id, {name:'new name'}) // upload const fileData = await driver.upload(newDir.id, stream,{ name,size }) // download try{ const { stream } = await driver.createReadStream(fileData.id) stream.pipe( fs.createReadStream('./'+fileData.name)) }catch(e){ } // remove await driver.rm(newDir.id) // Also you can use path locate const disk = driver.createAction() await disk.list('/') // mkdir create dir named 'new_dir' at '/' await disk.mkdir('/new_dir',) // rename await disk.mv('/new_dir','/new_dir2') // move await disk.mv('/new_dir','/some/new_dir') // rm await disk.rm('/some/new_dir') //upload await disk.upload('/some/newfile.txt',stream) ``` ================================================ FILE: packages/sharelist-core/index.js ================================================ module.exports = require('./lib/index') ================================================ FILE: packages/sharelist-core/lib/action.js ================================================ /** * 提供文件名寻址操作 * ls / rm / mkdir / stat / upload / get / mv(rename) */ const parsePath = v => v.replace(/(^\/|\/$)/g, '').split('/').map(decodeURIComponent).filter(Boolean) const getRange = (r, total) => { if (r) { let [, start, end] = r.match(/(\d*)-(\d*)/); start = start ? parseInt(start) : 0 end = end ? parseInt(end) : total - 1 return { start, end } } } const createHeaders = (data, { maxage, immutable, range } = { maxage: 0, immutable: false }) => { let fileSize = data.size let fileName = data.name let headers = {} headers['Last-Modified'] = new Date(data.mtime).toUTCString() if (range) { let { start, end } = range headers['Content-Range'] = `bytes ${start}-${end}/${fileSize}` headers['Content-Length'] = end - start + 1 headers['Accept-Ranges'] = 'bytes' } else { header['Content-Range'] = `bytes 0-${fileSize - 1}/${fileSize}` headers['Content-Length'] = fileSize } headers['Content-Disposition'] = `attachment;filename=${encodeURIComponent(fileName)}` return headers } const createAction = (driver, { useProxy, baseUrl } = {}) => ({ async ls(path) { let p = path.replace(/(^\/|\/$)/g, '') let data = await driver.list({ paths: p ? p.split('/').map(decodeURIComponent) : [], params: { perPage: 0, sort: ['name', 1] } }) return data?.files }, async stat(path) { try { return await driver.stat(parsePath(path)) } catch (error) { console.log(error) return { error } } }, async get(path, options) { let data = await driver.get({ paths: parsePath(path) }) if (!options.reqHeaders) options.reqHeaders = {} delete options.reqHeaders.connection options.reqHeaders['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' if (data && data.download_url && !data.extra.proxy && !useProxy()) { return { status: 302, body: data.download_url } } else { let range = getRange(options.reqHeaders.range, data.size) || { start: 0, end: data.size ? (data.size - 1) : '' } let { stream, status, headers, enableRanges = false } = await driver.createReadStream(data.id, range) let isReqRange = !!options.reqHeaders.range if (stream) { let options = enableRanges ? { range } : {} let resHeaders = headers || createHeaders(data, options) return { body: stream, status: status || (isReqRange && enableRanges ? 206 : 200), headers: resHeaders } } } }, async upload(path, stream, { size }) { stream.pause?.() let paths = parsePath(path) let name = paths.pop() let data = await driver.stat(paths) let existData = await driver.stat([...paths, name]) if (existData) { await driver.rm(existData.id) } if (!data.id) { return { error: { code: 404 } } } let ret = await driver.upload(data.id, stream, { name, size, hash: {}, state: {} }) if (!ret) { return { error: { code: 500 } } } else { return ret } }, async mkdir(path) { let paths = parsePath(path) let name = paths.pop() let parentData = await driver.stat(paths) return await driver.mkdir(parentData.id, name) }, async rm(path) { let paths = parsePath(path) let data = await driver.stat(paths) return await driver.rm(data.id) }, // /d/e/1.txt -> /d async mv(path, destPath, copy) { // The destination path can NOT be in the source path (include the same path) // e.g. /a/b => /a/b , /a/b => /a/b/c , ( /a/b => /a/b1, /a/b => /a/b1/c ) console.log('mv:', path, destPath) let paths = parsePath(path) let destPaths = parsePath(destPath) if ((destPaths.join('/') + '/').startsWith(paths.join('/' + '/'))) throw { code: 409 } let data = await driver.stat(paths) if (!data?.id) throw { code: 404 } let srcId = data.id let isSameDir = paths.length == destPaths.length && paths.slice(0, -1).join('/') == destPaths.slice(0, -1).join('/') // rename if (isSameDir) { if (paths.slice(-1)[0] == destPaths.slice(-1)[0]) throw { code: 409 } if (!copy) { await driver.rename(srcId, destPaths.slice(-1)[0]) return } } let dest = await driver.stat(destPaths) let destId, destName, srcName = paths.pop() //if destination exists if (dest?.id) { // destination must be a folder if (dest.type != 'folder' && dest.type != 'drive') throw { code: 409 } destId = dest.id } // destination does not exist else { let destParent = await driver.stat(destPaths.slice(0, -1)) //dest parent must be a folder if (destParent?.type != 'folder' && destParent?.type != 'drive') throw { code: 404 } destName = destPaths.pop() destId = destParent.id } let options = { copy } //move and rename if (destName && srcName != destName) options.name = destName console.log('move:', options) await driver.mv(srcId, destId, options) return { status: 201 } } }) module.exports = createAction ================================================ FILE: packages/sharelist-core/lib/driver.js ================================================ const utils = require('./utils') const request = require('./request') const { PassThrough } = require('stream') const clone = (obj) => { // console.log(obj) let type = typeof obj if (type == 'number' || type == 'string' || type == 'boolean' || type === undefined || type === null) { return obj } if (obj instanceof Array) { return obj.map((i) => clone(i)) } if (obj instanceof Object) { let copy = {} for (let attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]) } return copy } return obj } const sortFiles = (files, orderBy) => { let [key, isAsc] = orderBy let aVal = isAsc ? 1 : -1 let bVal = isAsc ? -1 : 1 return files.sort((a, b) => a[key] > b[key] ? aVal : bVal).sort((a, b) => { if (a.type == 'folder' && b.type != 'folder') { return -1 } else if (a.type != 'folder' && b.type == 'folder') { return 1 } else { return 0 } }) } /** * driver 采用 id 作为首选寻址方式 */ module.exports = (app) => { const getDrive = (id) => app.getDrive(id) /** * list * @param {object} options * @param {string} options.id * @param {array} options.paths * @param {object} options.params * @param {boolean} options.ignoreInterceptor * * @returns {array} */ const list = async ({ paths = [], id, params, ignoreInterceptor = false } = {}) => { await app.emit('beforeList', { paths, id, params }) let data = id ? await listById(id, params) : await listFromPathAddressing([...paths], params) /* if (params.orderBy && data.files) { data.files = sortFiles([...data.files], params.orderBy) } */ await app.emit('afterList', { paths, id, data, params }) return clone(data) } /** * @param {object} options * @param {string} options.id * @param {array} options.paths * * @returns {object} */ const get = async ({ paths = [], id }, options) => { let data if (!id && paths) { data = await stat(paths, more) id = data.id } if (id) { let r = await getById(id, options) if (!r.error) { data = r } } if (!data) return app.error({ code: 404 }) // if (data.extra && data.extra.path) { // let drive = root().files.find((i) => id.startsWith(i.id)) // data.path = drive.name + data.extra.path + '/' + data.name // } return { ...data } } /** * get file by paths * @param array paths * @returns */ const stat = async (paths) => { let parentPath = paths.slice(0, paths.length - 1) let filename = paths[paths.length - 1] let parent = await listFromPathAddressing(parentPath) if (parent.error) return undefined if (paths.length == 0) { return { id: parent.id, type: 'folder', name: '@sharelist_root', size: parent.size || Number.MAX_SAFE_INTEGER } } else { return parent?.files.find(i => i.name == filename) } } /** * 根据uri获取资源详情 * @param {*} uri * @returns */ const getById = async (uri, { more = false, enableCache = true } = {}) => { let { drive, encode, config, id, name } = await getDrive(uri) // if (isRoot(id)) { // return { // id: encode(id), // name, // type: 'folder' // } // } let cacheId = `${encode(id)}#get` if (config.cache !== false && enableCache) { let r = app.cache.get(cacheId) if (r) { console.log(`[CACHE] ${new Date().toISOString()} ${cacheId}`) return r } } if (!drive?.get) return { error: { message: '' } } let data = await drive.get(id, more) data.id = encode(data.id) //鉴于某些driver get 无法获取name和size,此处需通过 parent files 进行补充 if (!data.name && data.extra?.parent_id) { let parent = await listById(encode(data.extra.parent_id)) let last = parent.files.find(i => i.id == data.id) if (last) { data.name = last.name data.size = last.size if (last.type == 'file') { if (last.extra.md5) data.extra.md5 = last.extra.md5 if (last.extra.sha1) data.extra.sha1 = last.extra.sha1 } } } if (config.cache !== false) { let max_age = data.max_age || 0 if (max_age) { app.cache.set(cacheId, data, max_age) } } return data } /** * 获取可下载链接 * @param {*} uri * @returns */ const get_download_url = async (uri) => { let cacheId = `${uri}#download` let r = app.cache.get(cacheId) if (r) { console.log(`[CACHE] ${new Date().toISOString()} ${cacheId}`) return r } let { drive, config, id } = await getDrive(uri) if (drive?.get_download_url) { let data = await drive.get_download_url(id) if (data.url && config.cache !== false) { if (data.max_age) { app.cache.set(`${id}#download`, data, data.max_age) } } return data } } const getParentId = async (id) => { let data = await getById(id) return data?.extra?.parent_id } /** * 返回指定id的只读流 * @param {string} uri * @param {object} options * @param {object | undefined} options.reqHeaders * @param {number} options.start offset start * @param {number} options.end offset end * * @returns { stream , enableRanges , headers? , status? } */ const createReadStream = async (uri, options = {}) => { let { drive, id } = await getDrive(uri) let default_ua = app.config.default_ua console.log('dua', default_ua) if (drive?.createReadStream) { let stream = await drive.createReadStream(id, options) stream.once('error', () => { }) return { stream, enableRanges: true } } else { let data = await get({ id: uri }) if (data.download_url) { let { start, end, reqHeaders = {}, ...reqOptions } = options || {} if (data.extra.proxy?.headers) { Object.assign(reqHeaders, data.extra.proxy.headers) } if (data.extra.req_user_agent && !(reqHeaders['user-agent'] || reqHeaders['User-Agent'])) { reqHeaders['user-agent'] = default_ua } if (options.start !== undefined) { reqHeaders['range'] = `bytes=${start}-${end || ''}` } reqOptions.headers = reqHeaders reqOptions.responseType = 'stream' let { data: stream, headers, status, error } = await request(data.download_url, reqOptions) if (!error) { return { stream, headers, status, enableRanges: headers?.['accept-ranges'] == 'bytes' || status == 206 || headers?.['content-range'] } } } } throw { code: 501, message: "Not implemented" } } //获取文本内容 const getContent = async (id, charset = 'utf-8') => { try { let { stream } = await createReadStream(id) if (!stream) return null return await utils.transfromStreamToString(stream, charset) } catch (e) { return null } } /** * 返回指定可写流 * @param {string} uri * @param {object} options * @param {number} options.size * @param {string} options.name * @param {string} options.sha1? * @param {string} options.md5? * * @returns { stream:WritableStream , doneHandler:Function } * @public */ const createWriteStream = async (uri, options) => { let { drive, id, encode, config } = await getDrive(uri) if (drive?.upload) { let passStream = new PassThrough() let done = (cb) => { done.cb = cb } drive.upload(id, passStream, { ...options }).then(res => { done.cb?.(res) }) return { stream: passStream, done } } app.error({ code: 501, message: "Not implemented" }) } /** * upload * @param {string} uri * @param {stream | () => stream} stream * @param {object} options * @param {number} options.size * @param {string} options.name * @param {string} options.hash * @param {object} options.state * @param {string} options.conflictBehavior replace|rename|fail * * @returns {object} * * @public */ const upload = async (uri, stream, options) => { let { drive, id, config, encode } = await getDrive(uri) stream?.pause?.() if (drive?.upload) { if (!options.state) options.state = {} if (!options.hash) options.hash = {} let data = await drive.upload(id, stream, options) if (data.id) { data.id = encode(data.id) if (config.cache !== false) { app.cache.remove(`${encode(id)}#list`) } } return data } app.error({ code: 501, message: "Not implemented" }) } /** * mkdir * @param {string} uri * @param {string|Array} name * @param {object} options * @param {boolean} strict 严格模式,此模式会事先判断是否存在目录 * @returns {object} * * @public */ const mkdir = async (uri, name, options = {}, strict = false) => { let { drive, id, encode, config } = await getDrive(uri) if (drive?.mkdir) { if (typeof name == 'string') { name = [name] } let data while (name.length && id) { let curName = name.shift(), targetExist if (strict) { let dir = await drive.list(id) targetExist = dir.files?.find(i => i.name == curName) } if (targetExist) { data = { id: targetExist.id, name: targetExist.name, parent_id: id } } else { data = await drive.mkdir(id, curName, options) if (config.cache !== false) { app.cache.remove(`${encode(id)}#list`) } } if (data.id) id = data.id } if (data.id) data.id = encode(data.id) return data } app.error({ code: 501, message: "Not implemented" }) } /** * remove * @param {string|Array} uri * @param {object} options * @returns {object} * * @public */ const rm = async (uri) => { let { drive, id, encode, config } = await getDrive(uri) if (drive?.rm) { let data = await drive.rm(id) if (config.cache !== false) { // clear cache app.cache.remove(`${encode(id)}#get`) if (!data.parent_id) { data.parent_id = await getParentId(uri) } if (data.parent_id) app.cache.remove(`${encode(data.parent_id)}#list`) } return data } app.error({ code: 501, message: "Not implemented" }) } /** * rename * @param {string} uri * @param {string} name new file name * @returns {object} * * @public */ const rename = async (uri, name, options = {}) => { let { drive, id, encode, config } = await getDrive(uri) if (drive?.rename) { let data = await drive.rename(id, name, options) if (config.cache !== false) { // clear cache app.cache.remove(`${uri}#get`) //clear parent cache if (!data.parent_id) { data.parent_id = await getParentId(uri) } if (data.parent_id) app.cache.remove(`${encode(data.parent_id)}#list`) } return data } app.error({ code: 501, message: "Not implemented" }) } //only support same protocol const mv = async (uri, target_uri, options = {}) => { if (app.isSameDrive(uri, target_uri)) { let { drive, id, encode, config } = await getDrive(uri) if (drive?.mv) { let cache = config.cache !== false let { id: target_id } = await getDrive(target_uri) // get origin parent id let originParentId if (!options.copy) { originParentId = await getParentId(uri) } let data = await drive.mv(id, target_id, { ...options }) // clear cache if (cache) { //在有缓存的情况下 取得的是原位置的父级id if (!data.parent_id) { data.parent_id = target_id } //clear new parent cache app.cache.remove(`${encode(target_id)}#list`) //clear target cache app.cache.remove(`${uri}#get`) //clear origin parent cache if request move if (!options.copy) { originParentId = data.origin_parent_id || originParentId app.cache.remove(`${encode(originParentId)}#list`) } } return data } } app.error({ code: 501, message: "Not implemented" }) } const listById = async (uri, params = {}) => { let { drive, id, encode, config } = await getDrive(uri) let { pagination, ...options } = params if (app.config.per_page && pagination !== false) { options.perPage = app.config.per_page } const cacheable = !options.search && config.cache !== false let cacheId = `${encode(id)}#list` if (cacheable) { let r = app.cache.get(cacheId) if (!!r) { console.log(`[CACHE] ${new Date().toISOString()} ${cacheId}`) //adjust sort if (params.orderBy && r.files) { r.files = sortFiles([...r.files], params.orderBy) } return { ...r, config } } } if (!drive) return app.error({ code: 501, message: `Not implemented. (Resource URI:${uri})` }) let { id: realId, files, maxAge, nextPage } = await drive.list(id, options) files.forEach(i => { i.id = encode(i.id) if (i.extra?.parent_id) { i.extra.parent_id = encode(i.extra.parent_id) } }) let data = { id: encode(realId || id), files } // Cache will be disabled if enable pagination if (cacheable && files?.length > 0 && (!params.nextPage && !nextPage)) { let maxAgeDir = maxAge || config.maxAgeDir || app.config.max_age_dir || 0 if (maxAgeDir) { app.cache.set(cacheId, data, maxAgeDir) } } return { ...data, config, nextPage } } /* * Get data by path * * @param {string} [p] path id * @param {function} [interceptor] interceptor * @return {array|object} * @api private */ const listFromPathAddressing = async (paths, params) => { let hit = await app.getRoot(params) //扩展唯一的文件夹 if (app.config.expand_single_disk && hit.files && hit.files.length == 1) { paths.unshift(hit.files[0].name) } // root path if (paths.length == 0) { return hit } for (let i = 0; i < paths.length; i++) { if (hit.files) { hit = hit.files.find((j) => j.name == paths[i]) if (!hit) { return app.error({ code: 404, message: `Can't find [${paths[i]}] folder` }) } } //if (!drive) return app.error({ code: 501, message: "Not implemented" }) hit = await listById(hit.id, paths.length - 1 == i ? { ...params } : {}) await app.emit('afterList', { data: hit, params, paths: paths.slice(0, i + 1) }) } return hit } /** * 根据id 获取路径层级 * @param {*} uri * @returns */ const pwd = async (uri) => { let dirs = [] let { encode, protocol, name, drive, id } = await getDrive(uri) if (drive.pwd) { dirs = await drive.pwd(id) if (dirs) { dirs.forEach(i => { i.id = encode(i.id) }) } } else { while (id) { if (drive.isRoot(id)) break let data = await getById(encode(id)) if (!data) break dirs.unshift(data) if (!data?.extra?.parent_id || data.id == '@drive_root') break id = data?.extra.parent_id } } console.log('>>>>>pwd', dirs) return [{ id: 'root', name, type: 'folder' }, ...dirs.map(i => ({ id: i.id, name: i.name, type: i.type }))] // if(parent?.extra) } const pwd_path = async (path) => { } const clearSession = async (uri, data) => { let { drive, id } = await getDrive(uri) drive.clearSession?.(id, data) } const hashUpload = async (uri, { hash, name, size }) => { let { drive, id, config, encode } = await getDrive(uri) if (!config?.hashUpload) { app.error({ code: 429 }) } let hashType = typeof config.hashUpload == 'string' ? config.hashUpload : config.hashUpload.type let options = { name, state: {}, hash: { [hashType]: hash } } if (size) { options.size = size } let { completed, ...data } = await drive.upload(id, null, options) if (completed) { data.id = encode(data.id) if (config.cache !== false) { app.cache.remove(`${encode(id)}#list`) } return data } // } app.error({ code: 404, message: "file is non-exist" }) } return { list, get, stat, mkdir, rm, rename, mv, pwd, upload, hashUpload, clearSession, createReadStream, createWriteStream, get_download_url, getContent } } ================================================ FILE: packages/sharelist-core/lib/index.js ================================================ const { URL } = require('url') const utils = require('./utils') const createDriver = require('./driver') const actionFactory = require('./action') const request = require('./request') const { createRectifier, streamReader, createChunkStream } = require('./rectifier') const { isFunction, isClass, createCache } = utils const plugins = [] const DRIVE_KEY = Symbol('drive_key') const error = (error) => { console.trace(error) throw error } const isSameDrive = async (src, dest) => { let a = decode(src), b = decode(dest) return a.protocol === b.protocol && a.key === b.key } const decode = (p) => { let hasProtocol = p.includes('://') if (!hasProtocol) p = 'sharelist://' + p let data = new URL(p) let protocol = data.protocol.replace(':', '') let result = { protocol, key: data.host, path: decodeURIComponent(data.pathname || ''), } if (result.path) result.path = result.path.replace(/^\/+/, '') if (hasProtocol) result.protocol = data.protocol.split(':')[0] if (!result.path) result.path = undefined return result } const deduceConfig = (drive) => { return { readonly: !drive.upload } } const createSingleton = (plugin, inject) => { let { module, protocol } = plugin return { id: protocol, name: protocol, config: {}, configHash: '', encode: path => path, decode: path => path, drive: new module(inject), isRoot: () => true } } exports.createPluginLoader = async (options) => { const config = options.config || {} const cache = options.cache || createCache() //save index of driver in plugins array const driversMap = new Map() const loadDrives = (plugin, inject, override = false) => { let { module, protocol, options } = plugin const drivesConfig = config.drives.filter(i => i.protocol === protocol) const keyProp = options.key const defaultRoot = options.defaultRoot const activeDrives = [] drivesConfig.forEach(({ name, config, id }) => { let configHash = utils.hash(JSON.stringify(config)) let driveIndex = plugin.drives.findIndex(i => i.id == id) activeDrives.push(id) // if config has not changed OR driver has not changed. if (driveIndex >= 0 && plugin.drives[driveIndex].configHash == configHash && !override) { return } console.log(' - mount: ' + id) //privacy let getKey = () => { return utils.hash('sharelist_' + (config[DRIVE_KEY] || (keyProp ? config[keyProp] : id) || protocol)) } let configer = { get() { return { ...config } }, set(data) { for (let i in data) { config[i] = data[i] } } } const drive = new module(inject, config) // inject if (!drive.isRoot) { drive.isRoot = function (path) { return (config.root_id || defaultRoot) === path } } const driveMeta = { id, // drive name (the disk name set by user) name, // Each drive has its unique ID, but it's not suitable for use as cache ID. // Because they may use the same configuration. // so it MUST be provide a another key for driver plugin. getKey, // drive config config, // drive config hash configHash, // uri encode encode: (path) => { let pathname = path === undefined || path === '' ? '/' : '/' + path let ret = `${protocol}://${getKey() || ''}${pathname}` return ret }, // drive instance drive // drive: classMode ? new module(sharelist, drive.config) : module.call(module, sharelist, drive.config) } // update/create if (driveIndex >= 0) { plugin.drives[driveIndex] = driveMeta } else { plugin.drives.push(driveMeta) } }) //remove others for (let i = plugin.drives.length; i--;) { if (!activeDrives.includes(plugin.drives[i].id)) { console.log(' - unmount: ' + plugin.drives[i].id) plugin.drives[i].drive?.destroy?.() plugin.drives[i].drive = null plugin.drives[i].encode = null plugin.drives[i].getKey = null plugin.drives.splice(i, 1) } } // driver.drives = driver.drives.filter(i => existDrives.includes(i.id)) } /** * 获取指定协议的挂载驱动 */ const getDriver = (protocol) => { if (protocol) { let idx = driversMap.get(protocol) if (idx !== undefined) { return plugins[idx] } } else { return plugins.filter(i => i.isDriver).map(i => ({ name: i.name, protocol: i.protocol, guide: i.options.guide, mountable: i.options.mountable !== false })) } } /** * 根据URI获取磁盘元信息 */ const getDrive = async (uri) => { let { protocol, key, path } = decode(uri) let plugin = getDriver(protocol) if (!plugin) return {} let hit = plugin.singleton || plugin?.drives?.find(i => i.getKey() == key) if (!hit) return {} if (hit.decode) { path = hit.decode(uri) } let config = Object.assign(deduceConfig(hit.drive), plugin.options || {}, await hit?.drive?.getOptions?.() || {}) return { name: hit.name, drive: hit.drive, config, id: path || plugin.options?.defaultRoot, encode: hit.encode, protocol, isRoot: hit.isRoot } } const getRoot = async ({ orderBy } = {}) => { const disk = [] for (let i of config.drives) { let { id, protocol, name, config } = i let plugin = getDriver(protocol) let hit = plugin?.drives?.find(i => i.id == id) // sources 和 drive 未做区分 if (hit) { let config = Object.assign({}, plugin.options || {}, await hit?.drive?.getOptions?.() || {}) disk.push({ id: hit.encode(config.root_id || plugin.options?.defaultRoot),//protocol + '://' + drive.getKey(), name: name, size: 0, mtime: '', ctime: '', type: 'drive', config, extra: { config_id: id, } }) } } if (orderBy?.[0] == 'name') { let aVal = orderBy[1] ? 1 : -1 let bVal = aVal == 1 ? -1 : 1 disk.sort((a, b) => a.name > b.name ? aVal : bVal) } return { id: 'root:', type: 'drive', name: '', files: disk, config: { pagination: false, search: false, isRoot: true } } } // load plugin const load = async (newPlugins, override = false) => { if (!Array.isArray(newPlugins)) { newPlugins = [newPlugins] } // preprocessing: // plugins with the same name will be overwrittenexcept for drivers which differentiated by protocol. let validPlugins = [] for (let { name, module, hash } of newPlugins) { if (plugins.find(i => i.hash == hash)) { continue } if (isFunction(module)) { module = module(inject) } // plugin upgrade/downgrade let { driver, ...pluginData } = module pluginData.name = name pluginData.hash = hash if (!!driver) { let options = driver.options let idx = validPlugins.findIndex(i => i.protocol == options.protocol) pluginData.isDriver = true pluginData.options = options pluginData.drives = [] pluginData.protocol = options.protocol pluginData.module = driver if (idx >= 0) { validPlugins.splice(idx, 1) } } validPlugins.push(pluginData) } for (let plugin of validPlugins) { let { hash, isDriver } = plugin if (isDriver) { const protocol = plugin.protocol // driver must be update when: // 1. exist plugins that handle the same protocol. // 2. plugin content has been changed. let existPlugin = getDriver(protocol) const isDriverUpdated = !existPlugin || existPlugin.hash != hash if (isDriverUpdated) { console.log('load driver ' + protocol, plugins.length) driversMap.set(protocol, plugins.length) } //singleton if (plugin.options.singleton) { console.log(' - mount: singleton') plugin.singleton = createSingleton(plugin, inject) } else { loadDrives(plugin, inject, isDriverUpdated) } } else { let existIndex = plugins.findIndex(i => i.name === plugin.name && !i.isDriver) if (existIndex >= 0) { plugins.splice(existIndex, 1) } } plugins.push(plugin) } } const unload = (hashes) => { for (let i = plugins.length; i--;) { let plugin = plugins[i] let { hash } = plugin if (!hashes.includes(hash)) continue //driver plugin if (plugin.protocol) { plugin.drives.forEach(i => { console.log(' - unmount drive: ' + i.id) i.drive?.destroy?.() i.drive = null i.encode = null i.getKey = null }) driversMap.delete(plugin.protocol) } plugins.splice(i, 1) } } const loadConfig = () => { driversMap.forEach(i => { let plugin = plugins[i] if (plugin) { loadDrives(plugin, inject) } }) } const ocr = async (image, type, lang) => { if (config.ocr_server) { let { data } = await request.post(config.ocr_server, { method: 'post', contentType: 'json', data: { image } }) return { code: data.result } } return { error: { message: 'ocr server is NOT ready!' } } } const getPluginConfig = () => { const config = plugins.map((i) => i.config ? ({ name: i.name, config: i.config() }) : undefined).filter(Boolean) return config } const emit = async (type, ...rest) => { const fns = plugins.map((i) => i[type]).filter(Boolean) for (let fn of fns) { await fn(...rest) } } const inject = { DRIVE_KEY, config, cache, createCache, request, utils, error, ocr, isSameDrive, emit, getDriver, getDrive, getRoot, createRectifier, streamReader, createChunkStream } //抽象驱动 const driver = createDriver(inject) const createAction = (options) => actionFactory(driver, options) inject.driver = driver load(options.plugins) return { ...driver, createAction, load, unload, loadConfig, getPluginConfig, isSameDrive, getDriver } } exports.request = request ================================================ FILE: packages/sharelist-core/lib/rectifier.js ================================================ const { resolve4 } = require('dns') const { Readable, Writable, PassThrough } = require('stream') const request = require('./request') //限速 const throttleStream = (stream) => { stream.pause() setTimeout(() => { stream.resume() }, 1000) } exports.streamReader = function (readStream, { highWaterMark } = { highWaterMark: 10 * 1024 * 1024 }) { const buffers = []// 缓冲区 let total = 0 let tasks = [] let ended = false const sliceBuffer = (n) => { let part = Buffer.alloc(n) let b let index = 0 let flag = false while (null != (b = buffers.shift())) { for (let i = 0; i < b.length; i++) { part[index++] = b[i] if (index == n) {//填充完毕 //将多出的部分存回头部 b = b.slice(i + 1) buffers.unshift(b) flag = true break; } } if (flag) break; } total -= n return part } const check = () => { let task = tasks[0] if (task) { if (total >= task.size) { let chunk = sliceBuffer(task.size) task.done(chunk) tasks.shift() } else { if (ended) { let chunk = sliceBuffer(total) task.done(chunk) tasks.shift() } } } if (!ended) { if (tasks.length == 0 && total > highWaterMark) { // 直接暂停 可能导致客户端 timeout throttleStream(readStream) } else { if (readStream.isPaused()) { readStream.resume() } } } } const read = (size) => new Promise((resolve, reject) => { if (ended && total == 0) { resolve() } else { tasks.push({ size, done: resolve }) check() } }) readStream.on('data', (chunk) => { total += chunk.length buffers.push(chunk.slice(0)) check() }) readStream.on('end', () => { ended = true check() //let task = tasks.pop() }) return { read } } exports.rectifier = function (size = 1 * 1024 * 1024, cb) { const buffers = []// 缓冲区 let total = 0 let part = 0 let partSize = 0 let cacheRate = 3 const writable = new Writable({ write(chunk, encoding, callback) { let bytesRead = chunk.length partSize += bytesRead total += bytesRead buffers.push(chunk.slice(0)) //截取片段 if (partSize >= size) { let n = size let partBuffer = Buffer.alloc(n) let b let index = 0 let flag = false while (null != (b = buffers.shift())) { for (let i = 0; i < b.length; i++) { partBuffer[index++] = b[i] if (index == n) {//填充完毕 //将多出的部分存回头部 b = b.slice(i + 1) buffers.unshift(b) partSize = b.length flag = true break; } } if (flag) break; } // console.log('partSize', size, partSize, total) //缓冲 if (partSize < size * cacheRate) { callback() } cb({ total, chunk: partBuffer, chunk_index: part++ }, () => { //callback() }) } else { //继续写入 callback() } }, // 处理剩余的部分 final(callback) { let n = total - size * part // 文件大小是 size 的倍数时 if (n == 0) { callback() return } //处理掉剩余部分 let partBuffer = Buffer.alloc(n) let b let index = 0 while (null != (b = buffers.shift())) { for (let i = 0; i < b.length; i++) { partBuffer[index++] = b[i] } } cb({ total, chunk: partBuffer, chunk_index: part++, ended: true }, () => { callback() }) } }) return writable } const createStream = options => { const stream = new PassThrough({ highWaterMark: options?.highWaterMark || 1024 * 512, }); stream._destroy = () => { stream.destroyed = true; }; return stream; }; const retry = (run, retryTimes = 3, maxTime = 6000) => new Promise((resolve, reject) => { let lastError, time = 1 let error = (time) => new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), time)) const process = () => Promise.race([error(maxTime), run]).then(resolve).catch(e => { if (e?.message) lastError = e if (time++ <= retryTimes) { console.log('retry:', time) try { process() } catch (e) { reject(e) } } else { reject(lastError) } }) process() }) exports.createChunkStream = (url, chunkSize = 10 * 1024 * 1024, options = {}) => { let downloaded = 0 let stream = createStream(options.highWaterMark) let willEnd = false let pos = options.start || 0 let end = options.end || options.total || 0 const next = async () => { let rangeEnd = pos + chunkSize if (!end || rangeEnd >= end) { rangeEnd = end willEnd = true } let headers = { range: `bytes=${pos}-${rangeEnd || ''}`, ...(options.headers || {}) } let res = await retry(request.get(url, { headers, responseType: 'stream', retry: 0, timeout: 5000 }), 3, 6000) const ondata = chunk => { downloaded += chunk.length; stream.emit('progress', downloaded); }; res.data.on('data', ondata) res.data.on('error', (e) => { console.log(e) }) res.data.on('end', () => { if (!willEnd) { pos = rangeEnd + 1 next() } }) // stream res.data.pipe(stream, { end: willEnd }) } next() return stream } exports.createChunkWriteStream = function (url, chunkSize = 10 * 1024 * 1024, total, onChunk) { const stream = new PassThrough({ highWaterMark: 512 * 1024, }); let pos = 0 let uploaded = 0, chunkUploaded = 0 let exceed, uploadStream stream.on('data', (chunk) => { let willPause = false chunkUploaded += chunk.length if (chunkUploaded >= chunkSize) { exceed = chunk.slice(chunkSize) willPause = uploadStream.end(chunk.slice(0, chunkSize)) } else { willPause = uploadStream.write(chunk.slice(0, chunkSize)) } if (flag == false) { stream.pause() } }) stream.on('end', () => { }) const next = async () => { let rangeEnd = pos + chunkSize if (!end || rangeEnd >= end) { rangeEnd = end willEnd = true } uploadStream = new PassThrough() uploadStream.on('end', next) chunkUploaded = 0 if (exceed) { uploadStream.write(exceed) exceed = null } let reqOptions = { url, headers: {}, method: 'put', contentType: "stream", body: uploadStream, responseType: 'text', } if (onChunk) { let res = await onChunk(pos, rangeEnd) reqOptions = { ...reqOptions, ...res } } request(reqOptions.url, reqOptions) stream.resume() } next() return stream } ================================================ FILE: packages/sharelist-core/lib/request.js ================================================ const fetch = require('node-fetch') const btoa = (v) => Buffer.from(v).toString('base64') const https = require('https') const http = require('http') const { URL } = require('url') class LRUCache { constructor(size = 10) { this.size = size this.store = new Map() this.index = [] } update(key) { let index = this.index let idx = index.indexOf(key) let cur = index[idx] //update index index.splice(idx, 1) index.unshift(cur) } get(key) { const { store } = this // update if (store.has(key)) { this.update(key) return store.get(key) } } set(key, val) { const { store, index } = this if (store.has(key)) { this.update(key) } else { if (store.size >= this.size) { let delKey = index.pop() store.delete(delKey) } index.unshift(key) } store.set(key, val) } } const lruCache = new LRUCache() const createAgent = (proxy, isHttpsAgent = true) => { const proxyParsed = typeof proxy === 'string' ? new URL(proxy) : proxy const agent = isHttpsAgent ? new https.Agent() : new http.Agent() agent.proxy = proxyParsed agent.superCreateConnection = agent.createConnection agent.createConnection = function (options, callback) { const proxyParsed = this.proxy const isHttpsAgent = this instanceof https.Agent const isHttpsTunnel = proxyParsed.protocol === 'https:' const requestOptions = { method: 'CONNECT', host: proxyParsed.hostname, port: proxyParsed.port, path: `${options.host}:${options.port}`, setHost: false, headers: { connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, agent: false, timeout: options.timeout || 0 } if (proxyParsed.username || proxyParsed.password) { const base64 = Buffer.from(`${decodeURIComponent(proxyParsed.username || '')}:${decodeURIComponent(proxyParsed.password || '')}`).toString('base64') requestOptions.headers['proxy-authorization'] = `Basic ${base64}` } // Necessary for the TLS check with the proxy to succeed. if (isHttpsTunnel) { requestOptions.servername = this.proxy.hostname } //console.log('request', requestOptions) const request = (isHttpsTunnel ? https : http).request(requestOptions) request.once('connect', (response, socket, head) => { request.removeAllListeners() socket.removeAllListeners() if (response.statusCode === 200) { callback(null, isHttpsAgent ? this.superCreateConnection({ ...options, socket }) : socket) } else { callback(new Error(`Bad response: ${response.statusCode}`), null) } }) request.once('timeout', () => { request.destroy(new Error('Proxy timeout')) }) request.once('error', err => { request.removeAllListeners() callback(err, null) }) request.end() } return agent } const createProxyAgent = (proxy, isHttpsAgent = false) => { let key = (isHttpsAgent ? 'https' : 'http') + '+' + proxy let agent = lruCache.get(key) if (!agent) { try { agent = createAgent(proxy, isHttpsAgent) lruCache.set(key, agent) } catch (e) { console.log(e) return } } return agent } const retryTime = (times, maxBackOffTime = 6 * 1000) => { return Math.min(Math.pow(2, times) * 1000 + Math.floor(Math.random() * 1000), maxBackOffTime) } const sleep = time => new Promise((resolve, reject) => setTimeout(resolve, time)) // { // // These properties are part of the Fetch Standard // method: 'GET', // headers: {}, // request headers. format is the identical to that accepted by the Headers constructor (see below) // body: null, // request body. can be null, a string, a Buffer, a Blob, or a Node.js Readable stream // redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect // signal: null, // pass an instance of AbortSignal to optionally abort requests // // The following properties are node-fetch extensions // follow: 20, // maximum redirect count. 0 to not follow redirect // timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. // compress: true, // support gzip/deflate content encoding. false to disable // size: 0, // maximum response body size in bytes. 0 to disable // agent: null // http(s).Agent instance or function that returns an instance (see below) // } const each = (src, fn) => { let ret = {} for (let i in src) { ret[i.toLowerCase()] = fn(src[i], i) } return ret } const convToLowerCase = (props) => { let ret = {} Object.keys(props).forEach(key => { ret[key.toLowerCase()] = props[key] }) return ret } const qs = (data) => { let c = {} Object.keys(data).forEach(i => { c[i] = typeof data[i] == 'object' ? JSON.stringify(data[i]) : data[i] }) return new URLSearchParams(c) } const request = async (url, options = {}) => { let { data, method = 'GET', contentType, responseType = 'json', followRedirect = true, maxRedirects = 10, auth, headers = {}, agent, compress = false, timeout = 5000, retry = 2, proxy, ...rest } = options let args = { method: method.toUpperCase(), size: 0, agent, compress, timeout, headers: convToLowerCase(headers), ...rest } if (proxy) { args.agent = (urlParsed) => createProxyAgent(proxy, urlParsed.protocol === 'https:') } if (auth) { args.headers['authorization'] = `Basic ${btoa(auth)}` } if (!args.headers['user-agent']) { args.headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' } if (followRedirect) { args.redirect = 'follow' if (maxRedirects) args.follow = maxRedirects } else { args.redirect = 'manual' } if (data) { if (Buffer.isBuffer(data) || !!data?.pipe) { retry = 0 } if (['GET', 'HEAD', 'OPTIONS'].includes(args.method)) { url += (url.includes('?') ? '' : '?') + new URLSearchParams(data).toString() } else { if (contentType == 'json') { args.body = JSON.stringify(data) if (!args.headers['content-type']) { args.headers['content-type'] = 'application/json' } } else if (contentType == 'form') { args.body = qs(data) // if (!args.headers['content-type']) { // args.headers['content-type'] = 'application/x-www-form-urlencoded' // } } else { args.body = data args.timeout = 0 } } } let res, time = 0 while (true) { try { res = await fetch(url, args) break } catch (e) { console.log('request retry', retry, e) if (time++ < retry) { await sleep(retryTime(time)) // return { error: { message: '[' + e.code + '] The error occurred during the request.' } } } else { let type = e.code || e.type throw { message: '[' + type + '] The error occurred during the request.', type } } } } let status = res.status let resHeaders = each(res.headers.raw(), (val) => val.join(',')) if (responseType == 'json' || responseType == 'text' || responseType == 'buffer') { let data try { data = await res[responseType]() } catch (e) { console.error(e) } return { status, headers: resHeaders, data, } } else { return { status, headers: resHeaders, data: res.body } } } request.post = (url, options) => request(url, { ...options, method: 'POST' }) request.get = (url, options) => request(url, { ...options, method: 'GET' }) module.exports = request ================================================ FILE: packages/sharelist-core/lib/utils.js ================================================ const crypto = require('crypto') const { Transform } = require('node:stream') const mimeParse = require('mime') const YAML = require('yaml') const htmlEntity = require('html-entities') const isType = (type) => (obj) => Object.prototype.toString.call(obj) === `[object ${type}]` const isArray = isType('Array') const isObject = isType('Object') const isString = isType('String') const isDate = isType('Date') exports.hash = content => crypto.createHash('md5').update(content).digest("hex") exports.isClass = (fn) => typeof fn == 'function' && /^\s*class/.test(fn.toString()) exports.base64 = { encode: (v) => Buffer.from(v).toString('base64'), decode: (v) => Buffer.from(v, 'base64').toString(), } exports.btoa = (v) => Buffer.from(v).toString('base64') exports.atob = (v) => Buffer.from(v, 'base64').toString() exports.isFunction = (fn) => typeof fn == 'function' const datetime = (exports.datetime = (date, expr = 'iso') => { var a = new Date() if (isDate(date)) { a = date } else if (isString(date)) { try { a = new Date(date) } catch (e) { } } var y = a.getFullYear(), M = a.getMonth() + 1, d = a.getDate(), D = a.getDay(), h = a.getHours(), m = a.getMinutes(), s = a.getSeconds() function zeroize(v) { v = parseInt(v) return v < 10 ? '0' + v : v } if (expr === 'iso') { return a.toISOString() } else if (expr == 'ms') { return a.getTime() } return expr.replace(/(?:s{1,2}|m{1,2}|h{1,2}|d{1,2}|M{1,4}|y{1,4})/g, function (str) { switch (str) { case 's': return s case 'ss': return zeroize(s) case 'm': return m case 'mm': return zeroize(m) case 'h': return h case 'hh': return zeroize(h) case 'd': return d case 'dd': return zeroize(d) case 'M': return M case 'MM': return zeroize(M) case 'MMMM': return ['十二', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一'][m] + '月' case 'yy': return String(y).substr(2) case 'yyyy': return y default: return str.substr(1, str.length - 2) } }) }) exports.byte = (v) => { if (v === undefined || v === null || isNaN(v)) { return '-' } let lo = 0 while (v >= 1024) { v /= 1024 lo++ } return Math.floor(v * 100) / 100 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'][lo] } const byteMap = { B: 1, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12, PB: 1e15, EB: 1e18 } exports.retrieveByte = (v) => { if (/[\d\.]+\s*(B|KB|MB|GB|TB|PB|EB|K|M|G|T|P|E)/.test(v)) { let num = parseFloat(v) let unit = (v.match(/(B|KB|MB|GB|TB|PB|EB|K|M|G|T|P|E)/) || [''])[0] if (unit && num) { if (!unit.endsWith('B')) unit += 'B' return num * (byteMap[unit] || 0) } } return 0 } exports.extname = (p) => p.split('.').pop() exports.filetype = (v) => { if (v) v = v.toLowerCase() if (['mp4', 'mpeg', 'wmv', 'webm', 'avi', 'rmvb', 'mov', 'mkv', 'f4v', 'flv'].includes(v)) { return 'video' } else if (['mp3', 'm4a', 'wav', 'wma', 'ape', 'flac', 'ogg'].includes(v)) { return 'audio' } else if (['doc', 'docx', 'wps'].includes(v)) { return 'word' } else if (['pdf'].includes(v)) { return 'pdf' } else if (['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'pdf', 'txt', 'yaml', 'ini', 'cfg'].includes(v)) { return 'doc' } else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 'tif'].includes(v)) { return 'image' } else if (['zip', 'rar', '7z', 'tar', 'gz', 'gz2'].includes(v)) { return 'archive' } else { return 'other' } } exports.mime = (v) => mimeParse.getType(v) exports.timestamp = (v) => datetime(v, 'ms') exports.fit = (src, filter) => { return Object.keys(filter).every((key) => filter[key] === src[key]) } exports.useErrorHandle = (cb) => Promise.resolve(cb()) .then((data) => { return Promise.resolve({ data }) }) .catch((err) => { return Promise.resolve({ err }) }) exports.videoQuality = (text) => { return ( { LD: 480, SD: 576, HD: 720, FHD: 1080, QHD: 1440, UHD: 2160, }[text] || 1080 ) } exports.transfromStreamToString = (stream, encoding = 'utf-8') => new Promise((resolve, reject) => { let str = '' stream.on('data', (chunk) => { str += chunk.toString(encoding) }) stream.on('end', () => { console.log(str) resolve(str) }) }) exports.yaml = YAML exports.htmlEntity = htmlEntity exports.safeCall = async (fn, args = []) => { let res try { res = args ? await fn(...args) : await fn() } catch (err) { console.log(err) } return res } exports.createCache = () => { const data = {} const get = (id, creator) => { let ret = data[id] if (ret) { if (Date.now() > ret.expired_at) { delete data[id] } else { return ret.data } } } const set = (id, value, max_age) => { data[id] = { data: value, expired_at: Date.now() + max_age } return value } const has = (id) => { let ret = data[id] if (ret) { if (Date.now() > ret.expired_at) { delete data[id] } else { return true } } return false } const clear = () => { for (let key in data) { delete data[key] } } for (let key in data) { get(key, data) } return { get, set, clear, has } } exports.retryTime = (times, maxBackOffTime = 8 * 1000) => { return Math.min(Math.pow(2, times) * 1000 + Math.floor(Math.random() * 1000), maxBackOffTime) } exports.waitStreamFinish = (src, dst) => new Promise((resolve) => { dst.on('finish', () => resolve(true)).on('error', () => resolve(false)) src.pipe(dst) src.resume?.() }) exports.LRUCache = class { constructor(size = 10) { this.size = size this.store = new Map() this.index = [] } update(key) { let index = this.index let idx = index.indexOf(key) let cur = index[idx] //update index index.splice(idx, 1) index.unshift(cur) } get(key) { const { store } = this // update if (store.has(key)) { this.update(key) return store.get(key) } } set(key, val) { const { store, index } = this if (store.has(key)) { this.update(key) } else { if (store.size >= this.size) { let delKey = index.pop() store.delete(delKey) } index.unshift(key) } store.set(key, val) } } exports.createCache = (options = {}) => { const data = {} const defaultMaxAge = options.defaultMaxAge || 0 const get = (id) => { if (id === undefined) return data let ret = data[id] if (ret) { if (Date.now() > ret.$expiredAt) { delete data[id] } else { return ret.data } } } const set = (id, value, maxAge) => { data[id] = { data: value, $expiredAt: Date.now() + (maxAge || defaultMaxAge) } return value } const clear = (key) => { if (key) { delete data[key] } else { for (let key in data) { delete data[key] } } } const remove = (key) => { delete data[key] } const walk = () => { // remove expired data for (let key of Object.keys(data)) { get(key) } if (Object.keys(data).length > 0) { setTimeout(walk, 60 * 1000) } } walk() return { get, set, remove, clear } } exports.streamMonitor = (initData = {}) => { let lastTime = Date.now(), chunkLoaded = 0 let stats = { loaded: initData.loaded || 0, total: initData.total // parts: initData.parts || {} } let counter = 0 let setCount = (len, id) => { //分段计数 // stats.parts[id] += len //整体计数 stats.loaded += len chunkLoaded += len let timePass = Date.now() - lastTime if (timePass >= 1000) { stats.speed = Math.floor(chunkLoaded * 1000 / timePass) lastTime = Date.now() chunkLoaded = 0 initData.update?.({ ...stats }) } } const probe = () => { let id = counter++ // stats.parts[id] = 0 return new Transform({ transform(chunk, encoding, callback) { // ... setCount(chunk.length, id) callback(null, chunk) } }) }; return { stats, probe } } ================================================ FILE: packages/sharelist-core/package.json ================================================ { "name": "@sharelist/core", "version": "0.2.0", "repository": "https://github.com/reruin/sharelist", "license": "MIT", "main": "index.js", "scripts": { "build": "echo \"Info: no specified\" && exit ", "test": "node test/index.js", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .", "release": "node ../../scripts/release.js --commit-path ." }, "dependencies": { "html-entities": "^2.3.3", "mime": "^2.5.2", "node-fetch": "^2.6.1", "querystringify": "^2.2.0", "write-file-atomic": "^3.0.3", "yaml": "^1.10.2" }, "devDependencies": {} } ================================================ FILE: packages/sharelist-core/test/index.js ================================================ const createDriver = require('../') const path = require('path') const testdriver = () => { const list = (id) => { return { id: 'folder', files: new Array(10).fill(0).map((i) => ({ id: 'folder' + i, name: 'file' + i, size: 0, type: 'folder', ctime: Date.now(), mtime: Date.now(), })), } } const get = (id) => { return { id: id, name: 'file' + id, size: 0, type: 'file', ctime: Date.now(), mtime: Date.now(), } } return { name: 'test', protocol: 'test', mountable: true, list, get } }; (async () => { const driver = await createDriver({ plugins: [testdriver] }) console.log(await driver.list()) })() ================================================ FILE: packages/sharelist-core/test/plugin/driver.test.js ================================================ modules.export = () => { const list = (id) => { return { id: 'folder', files: new Array(10).fill(0).map((i) => ({ id: 'folder' + i, name: 'file' + i, size: 0, type: 'folder', ctime: Date.now(), mtime: Date.now(), })), } } const get = (id) => { return { id: id, name: 'file' + id, size: 0, type: 'file', ctime: Date.now(), mtime: Date.now(), } } return { name: 'test', protocol: 'test', mountable: true, list, get } } ================================================ FILE: packages/sharelist-manage/.eslintrc.js ================================================ module.exports = { parser: 'vue-eslint-parser', parserOptions: { // set script parser parser: '@typescript-eslint/parser', // Specifies the ESLint parser ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features sourceType: 'module', // Allows for the use of imports ecmaFeatures: { jsx: true, // Allows for the parsing of JSX }, validate: [], }, extends: [ 'plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 'plugin:prettier/recommended', ], rules: { // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-function': 'off', // 'object-curly-spacing': ['error', 'always'], }, } ================================================ FILE: packages/sharelist-manage/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local yarn-error.log package-lock.json yarn.lock ================================================ FILE: packages/sharelist-manage/.prettierrc.js ================================================ // https://prettier.io/docs/en/configuration.html module.exports = { //分号终止符 semi: false, //行尾逗号 trailingComma: "all", // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号) singleQuote: true, printWidth: 120, // 换行符 endOfLine: "auto", //缩进 default:2 tabWidth: 2 } ================================================ FILE: packages/sharelist-manage/CHANGELOG.md ================================================ ================================================ FILE: packages/sharelist-manage/README.md ================================================ # @sharelist/web [![npm](https://img.shields.io/npm/v/@sharelist/web.svg)](https://npmjs.com/package/@sharelist/web) It's a part for sharelist ## Useage ================================================ FILE: packages/sharelist-manage/package.json ================================================ { "name": "@sharelist/manage", "version": "0.2.0", "scripts": { "dev": "vite", "build": "vite build", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .", "release": "node ../../scripts/release.js --skipBuild --skipNpmPublish" }, "dependencies": { "@ant-design/icons-vue": "^6.0.1", "@codemirror/basic-setup": "^0.19.1", "@codemirror/commands": "^0.19.7", "@codemirror/lang-javascript": "^0.19.6", "@codemirror/view": "^0.19.39", "@types/crypto-js": "^4.1.1", "@types/spark-md5": "^3.0.2", "ant-design-vue": "^3.2.5", "axios": "^0.27.2", "js-sha1": "^0.6.0", "pinia": "^2.0.14", "pinia-plugin-persist": "^1.0.0", "spark-md5": "^3.0.2", "vue": "3.x", "vue-router": "^4.0.15" }, "devDependencies": { "@types/node": "^15.x", "@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/parser": "^4.23.0", "@vitejs/plugin-legacy": "2.x", "@vitejs/plugin-vue-jsx": "2.x", "@vue/compiler-sfc": "^3.0.5", "eslint": "^7.26.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-vue": "^7.9.0", "less": "^4.1.1", "minimist": "^1.2.5", "prettier": "^2.3.0", "terser": "^5.15.0", "typescript": "^4.1.3", "unplugin-vue-components": "0.x", "vite": "3.x", "vue-eslint-parser": "^7.6.0", "vue-tsc": "^0.0.24" } } ================================================ FILE: packages/sharelist-manage/src/App.tsx ================================================ import { defineComponent } from 'vue' import { ConfigProvider } from 'ant-design-vue' import { RouterView } from 'vue-router' import zhCN from 'ant-design-vue/es/locale/zh_CN'; export default defineComponent({ setup() { return () => ( ) }, }) ================================================ FILE: packages/sharelist-manage/src/assets/style/index.less ================================================ // @import './icon.less'; @import 'ant-design-vue/lib/style/variable.less'; @import 'ant-design-vue/lib/button/style/index-pure.less'; @import 'ant-design-vue/lib/radio/style/index-pure.less'; @import 'ant-design-vue/lib/breadcrumb/style/index-pure.less'; @import 'ant-design-vue/lib/input/style/index-pure.less'; @import 'ant-design-vue/lib/input-number/style/index-pure.less'; @import 'ant-design-vue/lib/checkbox/style/index-pure.less'; @import 'ant-design-vue/lib/message/style/index-pure.less'; @import 'ant-design-vue/lib/modal/style/index-pure.less'; @import 'ant-design-vue/lib/spin/style/index-pure.less'; @import 'ant-design-vue/lib/switch/style/index-pure.less'; @import 'ant-design-vue/lib/menu/style/index-pure.less'; @import 'ant-design-vue/lib/dropdown/style/index-pure.less'; @import 'ant-design-vue/lib/list/style/index-pure.less'; @import 'ant-design-vue/lib/badge/style/index-pure.less'; @import 'ant-design-vue/lib/progress/style/index-pure.less'; @import 'ant-design-vue/lib/empty/style/index-pure.less'; @import 'ant-design-vue/lib/tabs/style/index-pure.less'; @import 'ant-design-vue/lib/form/style/index-pure.less'; @import 'ant-design-vue/lib/select/style/index-pure.less'; @import 'ant-design-vue/lib/popover/style/index-pure.less'; @import 'ant-design-vue/lib/alert/style/index-pure.less'; @import 'ant-design-vue/lib/tooltip/style/index-pure.less'; @import 'ant-design-vue/lib/image/style/index-pure.less'; @import 'ant-design-vue/lib/radio/style/index-pure.less'; @import './var.less'; @font-face { font-family: 'Source Code Pro'; font-style: normal; font-weight: 400; src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(data:application/x-font-woff2;charset=utf-8;base64,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) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } body{ -webkit-app-region: drag; // padding:5px; background: var(--background); color:var(--primary-text-color); font-size:14px; font-family: 'Source Code Pro'; } h1,h2,h3,h4,h5,h6{ color:var(--primary-text-color); } ul,ol{ padding-left:0; list-style: none; &.split{ display: flex; align-items: center; li{ position: relative; display: inline-block; } li:after{ content:''; position: absolute; top: 50%; right: 0; width: 1px; height: 10px; margin-top: -5px; background-color: #f0f0f0; } } } .no-drag,input,button{ -webkit-app-region: no-drag; } .flex{ display: flex; align-items: center; &.flex--between{ justify-content: space-between; } &.flex--block{ width:100%; } } ::-webkit-scrollbar-track { // box-shadow: inset 0 0 1px rgba(0,0,0,0.2); background-color: #f1f1f1; } ::-webkit-scrollbar { width: 5px; // background-color: rgba(200,200,200,1); } ::-webkit-scrollbar-thumb { background-color: rgba(200,200,200,.6); } .pure-modal{ z-index:1001; .ant-modal-content{ border-radius: 5px;} .ant-modal-body{ padding:16px 20px;} .anticon-exclamation-circle, .anticon-info-circle { display: none;} .ant-modal-confirm-title{ // border-bottom:1px solid #ddd; padding-bottom: 12px; // width: fit-content } .ant-modal-confirm-content{ margin-left:0 !important; } .modal-header{ padding:16px; } .modal-footer{ margin-top:16px; text-align: right; } .ant-modal-confirm-btns{ .ant-btn{ padding:0 2.5em; } } &.pure-modal-hide-footer{ .ant-modal-confirm-btns{ display: none; } } } .fix-modal--alone{ .ant-modal-body{ padding:16px;} .ant-modal-confirm-btns,.anticon-exclamation-circle{ display: none;} .ant-modal-confirm-content{ margin-top: 0; } .ant-modal-content{ border-radius: 6px; } } .fix-form--inline{ display: flex; flex-wrap: wrap; .ant-form-item{ flex:0 0 50%;padding:0 16px; &.fix-form-item--foot{ flex:100%; margin-bottom: 0; } } } .ant-tabs-tab{ padding:16px 0; } .ant-dropdown-menu{ background-color:var(--context-background); .ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover{ background-color: var(--context-hover); } .ant-dropdown-menu-item-group-title{ color: var(--context-3); } } .ant-checkbox-inner{ background-color:var(--context-background); } .ant-select-dropdown{ background-color:var(--context-background); .ant-select-item{ color:var(--primary-text-color); } .ant-select-item-option-active:not(.ant-select-item-option-disabled){ background-color: var(--context-hover); } } .ant-select{ color:var(--primary-text-color); } .ant-select:not(.ant-select-customize-input) .ant-select-selector{ border-color:var(--divider-3); background-color:var(--divider-3); } .ant-alert-info{ // border-color:var(--primary-color); border:none; background-color:var(--primary-color-bg); border-radius: 8px; .ant-alert-message{ // color:var(--primary-color); color: var(--context-2); font-size:12px; } } .ant-popover-inner,.ant-popover-arrow-content,.ant-modal-content{ background-color:var(--context-background); } .ant-tabs,.ant-modal-confirm-body .ant-modal-confirm-title,.ant-modal-confirm-body .ant-modal-confirm-content{ color:var(--primary-text-color); } .ant-tabs-top>.ant-tabs-nav:before{ border-color:var(--divider-2); } .ant-list-empty-text,.ant-empty-normal{ color:var(--primary-text-color); } .ant-badge,.ant-radio-wrapper{ color:var(--context-1); } .ant-switch{ background-color:var(--context-3); } .ant-switch-checked { background-color: var(--ant-primary-color); } .ant-input,.ant-input-number{ background-color:var(--divider-3); border-color:var(--divider-2); color:var(--primary-text-color); } .ant-input-affix-wrapper{ background-color:var(--divider-3); border-color:var(--divider-2); color:var(--primary-text-color); .ant-input{ background-color: transparent; } .ant-input-suffix,.ant-input-password-icon{ color:var(--primary-text-color); } } .ant-message-notice-content{ background-color:var(--background); color:var(--primary-text-color); } .ant-form-item-label>label{ color:var(--primary-text-color); } .ant-progress-text{ color:var(--primary-text-color); } .ant-empty-description{ color:var(--context-2); } .ant-btn-primary{ // background-color: var(--ant-primary-color-outline); // color:var(--ant-primary-color); // border:none; padding:0 3em; border-radius: 3px; } @media screen and (max-width:480px) { .fix-modal--alone{ top:0; } .fix-form--inline{ .ant-form-item{flex:100%;} } } .page{ .page__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2em; color: var(--primary-text-color); font-size: 18px; font-weight: 600; em{ font-size:12px; color:rgba(0,0,0,.45); margin-left:5px; font-style: normal; font-weight: normal; } .page__action{ a{ color:var(--primary-text-color); &:hover{ color:var(--primary-color); } } } } } F{ width:160px; border-radius: 5px; .dropdown-item{ padding-top:8px; padding-bottom:8px; color:var(--primary-text-color); } .ant-dropdown-menu{ border-radius: 5px; } .ant-dropdown-menu-item-group-list{ margin:0; } } .ant-modal-mask,.ant-modal-wrap{ z-index: 1030; } .sl-input{ border-radius: 6px; // box-shadow: 0 1px 5px rgb(0 0 0 / 12%); border-color:transparent; } .popover-padding-0{ .ant-popover-inner-content{ padding:0;} } .ellipsis-2{ overflow : hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; text-overflow: ellipsis; word-break: break-all; } .ellipsis-1{ overflow : hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; text-overflow: ellipsis; word-break: break-all; } .ellipsis{ overflow:hidden; text-overflow: ellipsis; white-space: nowrap; } .danger-aciton{ color:var(--danger-color); } .danger-aciton--hover:hover{ color:var(--danger-color) !important; } ================================================ FILE: packages/sharelist-manage/src/assets/style/var.less ================================================ :root{ // --theme:100,52,248; --theme:100,58,218; --primary-color:rgb(var(--theme)); //rgb(105,65,199); --primary-color-bg:rgba(var(--theme),.1); //rgb(105,65,199); --primary-text-color:rgb(37, 38, 43); --primary-background:rgb(var(--theme)); // --icon-folder-color:var(--primary-color); // --icon-other-color:var(--primary-color); // --icon-audio-color:rgb(132,140,239); // --icon-video-color:rgb(219,68,55); // --icon-word-color:rgb(47,151,254); // --icon-pdf-color:rgb(252,134,132); // --icon-ppt-color:rgb(254,159,93); // --icon-file-color:rgb(212,214,218); // --icon-word-color:rgb(88,178,252); // --icon-doc-color:rgb(88,178,252); // --icon-image-color:rgb(252,132,129); --primary-text-secondary-color:rgba(37, 38, 43,0.36); --color-main:var(--primary-text-color); --primary-hover-bg-color:rgba(132, 133, 141, 0.08); --primary-hover-theme-color:rgba(var(--theme), 0.08); --dark-background:rgb(49, 49, 54); --color-white-1:rgb(255,255,255); --color-white-2:rgba(255,255,255,0.72); --color-white-3:rgba(255,255,255,0.36); --color-white-4:rgba(255,255,255,0.18); --color-red:rgb(243, 91, 81); --background:#ffffff; --background-dark:rgb(9,9,10); --primary-text-color-dark:rgb(253,253,253); --primary-progress:rgba(var(--theme),0.1); --context:37, 38, 43;//0,0,0; --context-1:rgb(var(--context)); --context-2:rgba(var(--context),0.72); --context-3:rgba(var(--context),0.36); --context-4:rgba(var(--context),0.18); --context-background:#fff; --context-background--dark:rgb(49,49,54); --background-cover:rgb(248,249,252); --context-hover:#f5f5f5; --context-hover--dark:rgba(132, 133, 141,0.12);; --primary-text-color--dark:rgb(37, 38, 43); --divider: 132, 133, 141; --divider-1: rgba(var(--divider), 0.2); --divider-2: rgba(var(--divider), 0.16); --divider-3: rgba(var(--divider), 0.08); --danger-color:#ff4d4f; //player --plyr-color-main:#00b3ff; --plyr-audio-controls-background:rgba(0,0,0,0); --plyr-audio-control-color:#ddd; --plyr-control-radius:10px; --plyr-audio-background:rgb(49,49,54); --plyr-audio-control-background-hover:transparent; --plyr-video-control-background-hover:transparent; --plyr-video-background:rgba(0,0,0,0); --player-color-main:#fff; --player-hover-background:rgba(255,255,255,.1); --player-fill:rgba(0,179,255,.5); --plyr-menu-background:rgb(49,49,54); // --plyr-audio-control-color-hover:#aaa; --plyr-menu-color:#aaa; --plyr-menu-back-border-color:rgba(255,255,255,0.06); --plyr-menu-back-border-shadow-color:rgba(255,255,255,0.06); } @media (prefers-color-scheme: dark){ *{ color-scheme:dark; } } @media (prefers-color-scheme: dark){ // :root{ // --background:var(--background-dark); // --primary-text-color:var(--primary-text-color-dark); // --context:255,255,255; // --context-background:var(--context-background--dark); // --context-hover:var(--context-hover--dark); // --primary-progress:rgba(var(--theme),0.2); // } } ================================================ FILE: packages/sharelist-manage/src/components/code-editor/index.less ================================================ .cm-editor { max-height: 500px; border: 1px solid silver; font-size: 14px } .cm-scroller { overflow: auto; } .cm-content{ font-size:12px;} /* Code highlighting styles */ .hl-keyword, .fn, .keyword {color: #708;} .hl-atom {color: #219;} .hl-number, .prim {color: #164;} .hl-def, .hl-attribute {color: #30b;} .hl-variable-2, .hl-type, .type {color: #05a;} .hl-comment {color: #a50;} .hl-string, .string {color: #a11;} .hl-string-2 {color: #f50;} .hl-meta {color: #555;} .hl-tag {color: #170;} ================================================ FILE: packages/sharelist-manage/src/components/code-editor/index.tsx ================================================ import './index.less' import { ref, defineComponent, watch, onMounted, onUnmounted, toRefs, reactive, watchEffect } from 'vue' import { javascript } from "@codemirror/lang-javascript" import { EditorState, EditorView, basicSetup } from "@codemirror/basic-setup" export default defineComponent({ emits: ['update'], props: { defaultValue: { type: String, } }, setup(props, ctx) { const el = ref() const updateListenerExtension = EditorView.updateListener.of((update) => { if (update.docChanged) { // Handle the event here ctx.emit('update', update.state.doc.toString()) } }); let startState = EditorState.create({ doc: props.defaultValue, extensions: [basicSetup, javascript(), updateListenerExtension] }) let view onMounted(() => { view = new EditorView({ state: startState, parent: el.value }) }) onUnmounted(() => { }) return () =>
} }) ================================================ FILE: packages/sharelist-manage/src/components/icon/icon-svg.js ================================================ !(function (t) { var a, l, o, i, e, c, h = '', d = (d = document.getElementsByTagName('script'))[d.length - 1].getAttribute('data-injectcss') if (d && !t.__iconfont__svg__cssinject__) { t.__iconfont__svg__cssinject__ = !0 try { document.write( '', ) } catch (t) { console && console.log(t) } } function n() { e || ((e = !0), o()) } ; (a = function () { var t, a, l ; ((l = document.createElement('div')).innerHTML = h), (h = null), (a = l.getElementsByTagName('svg')[0]) && (a.setAttribute('aria-hidden', 'true'), (a.style.position = 'absolute'), (a.style.width = 0), (a.style.height = 0), (a.style.overflow = 'hidden'), (t = a), (l = document.body).firstChild ? (a = l.firstChild).parentNode.insertBefore(t, a) : l.appendChild(t)) }), document.addEventListener ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState) ? setTimeout(a, 0) : ((l = function () { document.removeEventListener('DOMContentLoaded', l, !1), a() }), document.addEventListener('DOMContentLoaded', l, !1)) : document.attachEvent && ((o = a), (i = t.document), (e = !1), (c = function () { try { i.documentElement.doScroll('left') } catch (t) { return void setTimeout(c, 50) } n() })(), (i.onreadystatechange = function () { 'complete' == i.readyState && ((i.onreadystatechange = null), n()) })) })(window) ================================================ FILE: packages/sharelist-manage/src/components/icon/index.less ================================================ // .sl-icon { // display: inline-block; // font-style: normal; // vertical-align: -0.125em; // text-align: center; // text-transform: none; // line-height: 0; // text-rendering: optimizeLegibility; // -webkit-font-smoothing: antialiased; // } /* --icon-folder-color:var(--primary-color); --icon-audio-color:rgb(132,140,239); --icon-video-color:rgb(219,68,55); --icon-word-color:rgb(47,151,254); --icon-pdf-color:rgb(252,134,132); --icon-ppt-color:rgb(254,159,93); --icon-file-color:rgb(212,214,218); --icon-word-color:rgb(88,178,252); --icon-doc-color:rgb(88,178,252); --icon-image-color:rgb(252,132,129); */ @type: { icon-folder:#f8d673; icon-file:rgb(188,190,194);//rgb(212,214,218); icon-audio:rgb(223,94,83); icon-video:rgb(223,94,83); icon-image:rgb(223,94,83); icon-word:rgb(96,181,252); icon-doc:rgb(96,181,252); icon-code:rgb(96,181,252); icon-ppt:rgb(254,173,96); icon-pdf:rgb(252,134,132); } // :root{ // each(@type, { // --@{key}: @value; // }); // } each(@type, { #@{key}{ color:~"var(--@{key}-color,@{value})"; } }); ================================================ FILE: packages/sharelist-manage/src/components/icon/index.ts ================================================ import { createFromIconfontCN } from '@ant-design/icons-vue' import config from '../../config/setting' import './icon-svg' import './index.less' const IconFont = createFromIconfontCN({ scriptUrl: [], }) export default IconFont ================================================ FILE: packages/sharelist-manage/src/components/image/index.less ================================================ .hide-modal{ display: none; } ================================================ FILE: packages/sharelist-manage/src/components/image/index.tsx ================================================ import { Image, Modal } from 'ant-design-vue' export const showImage = (urls: Array, index: number) => { const onVisibleChange = (e: any) => { if (e === false) { modal.destroy() } } const modal = Modal.confirm({ style: { display: 'none' }, width: '500px', closable: true, content: ( { urls.map((url, idx) => ) } ), }) } ================================================ FILE: packages/sharelist-manage/src/components/modal/index.less ================================================ .hide--modal{ .ant-modal-confirm-btns,.anticon-exclamation-circle{ display: none; } .ant-modal-body{ padding:0; } } .modal{ .modal__header{ padding:16px; } .modal__body{ } } ================================================ FILE: packages/sharelist-manage/src/components/modal/index.tsx ================================================ import { Modal } from 'ant-design-vue' import './index.less' export const modal = function (options: any) { const { content, title, ...rest } = options const node = () => Modal.confirm({ ...rest, class: 'hide--modal', content: node }) } ================================================ FILE: packages/sharelist-manage/src/components/player/index.less ================================================ .widget-player{ position: fixed; bottom:0; opacity: 0; pointer-events: none; // transform:translate(-50%,0); transition:all 0.3s; // left:50%; max-width: 560px; left:0;right:0; margin:auto; &.widget-player--visible{ opacity: 1; pointer-events: auto; bottom:16px; } .widget-player__tip{ opacity: .64; font-size:10px; } .widget-player__action{ display: flex; align-items: center; flex:none; } .widget-player__progress{ position: absolute; display: none; width: 0%; height:3px; left:0; bottom: 0; background-color: var(--plyr-color-main); transition:all 0.3s; } } .widget-player__list{ // max-height: 0; height: 0; display: flex; flex-direction: column; transition:all .5s cubic-bezier(0.66, 0, 0.01, 1); opacity: 0; &.widget-player__list--visible{ height: 260px; opacity: 1; } .widget-player__list-header{ color:#fff; padding:8px; text-align: center; border-bottom:1px solid rgba(132,133,141,.2); display:flex; align-items: center; justify-content: center; } .widget-player__list-body{ overflow-y: auto; overflow-x: hidden; color:var(--player-color-main); font-size: 13px; margin-bottom: 0; &::-webkit-scrollbar-track { // box-shadow: inset 0 0 1px rgba(0,0,0,0.2); background-color: rgba(0,0,0,0); } &::-webkit-scrollbar { width: 5px; // background-color: rgba(200,200,200,1); } &::-webkit-scrollbar-thumb { background-color: rgba(200,200,200,.6); } } .widget-player__list-item{ display: flex; align-items: center; padding:6px 16px; cursor: pointer; transition: all 0.3s; &:hover{ background-color: var(--player-hover-background,rgba(0,0,0,0)); } .widget-player__list-no{ flex:none; padding-right: 8px; text-align: right; width:36px; } } .widget-player__list-item--playing{ background-color: var(--player-fill,rgba(0,0,0,0)) !important; } } .widget-player-wrap{ box-shadow: 0 1px 5px rgba(0,0,0,0.2); background-color:var(--plyr-audio-background,#fff); border-radius: 8px; // overflow: hidden; .widget-player__content{ display: none; padding:0 12px; color:var(--player-color-main); } .widget-player__body{ display: flex; align-items: center; justify-content: space-between; flex:none; position: relative; z-index:1; // background-color:var(--plyr-audio-background,#fff); .widget-player__toggle-expand{ font-size:18px; padding:0 12px; cursor: pointer; color:var(--plyr-audio-control-color,#000); } .widget-player__close{ flex:none; font-size:18px; padding:0 12px; cursor: pointer; color:var(--plyr-audio-control-color,#000); } .widget-player__btn-full{ flex:none; font-size:18px; padding:0 12px; cursor: pointer; color:var(--plyr-audio-control-color,#000); display: none; } .widget-player__download{ flex:none; font-size:18px; padding:0 12px; cursor: pointer; color:var(--plyr-audio-control-color,#000); } } } .widget-player-audio{ .plyr{ min-width:300px; } } .widget-player-video{ overflow: hidden; .widget-player__progress{ display: block; } .widget-player__content{ display: block; cursor: pointer; min-width:200px; .widget-player__content-title{ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } } .widget-player__btn-full{ display: block !important; } &.widget-player--mini{ .plyr{ width:33.3%; height:70px; min-width:100px; flex:none; .plyr__controls{ display: none; } video{ object-fit: cover; } .plyr__video-wrapper{ margin: 0; height: 100%; } } } } .app-light{ --plyr-color-main:#00b3ff; --plyr-audio-controls-background:rgba(0,0,0,0); --plyr-audio-control-color:#ddd; --plyr-control-radius:10px; --plyr-audio-background:rgb(49,49,54); --plyr-audio-control-background-hover:transparent; --plyr-video-control-background-hover:transparent; --plyr-video-background:rgba(0,0,0,0); --player-color-main:#fff; --player-hover-background:rgba(255,255,255,.1); --player-fill:rgba(0,179,255,.5); &::-webkit-scrollbar-track { // box-shadow: inset 0 0 1px rgba(0,0,0,0.2); background-color: #000; } &::-webkit-scrollbar { width: 5px; // background-color: rgba(200,200,200,1); } &::-webkit-scrollbar-thumb { background-color: rgba(200,200,200,.6); } } @media screen and (max-width:480px) { .widget-player{ transform:translate(0,0); left:0;width:100%; bottom:-16px; .widget-player-wrap{ border-radius: 0; } .widget-player-audio .plyr{ width:auto; min-width:0px; } &.widget-player--visible{ bottom:0; } .widget-player__content{ min-width: 0; } } } ================================================ FILE: packages/sharelist-manage/src/components/player/index.tsx ================================================ import Plyr from 'plyr' import { ref, reactive, defineComponent, onMounted, onUnmounted, computed, watch, watchEffect } from 'vue' import 'plyr/dist/plyr.css' import './index.less' import { OrderedListOutlined, CloseOutlined, FullscreenOutlined, DownloadOutlined, DownOutlined } from '@ant-design/icons-vue' import { useBoolean, useState } from '@/hooks/useHooks' const playerMap = new Map() export const usePlayer = (id?: number | string): any => { if (id && playerMap.has(id)) { return playerMap.get(id) } const newId = id || playerMap.size + 1 const removePlayer = () => playerMap.delete(id) const [state, setPlayer] = useState({ list: [], type: '', index: 0, cur: { name: '', ctimeDisplay: '' }, }) const instance = { id: newId, data: state, setPlayer, removePlayer } playerMap.set(newId, instance) return instance } export default defineComponent({ props: { meidaId: { type: String, required: true, }, }, setup(props, ctx) { const el = ref() const { data, removePlayer } = usePlayer(props.meidaId) const [visible, { setFalse: hidePlayer, setTrue: showPlayer }] = useBoolean() const [visibleList, { toggle: toggleList }] = useBoolean() const [fullscreen, { setFalse: existFullScreen, setTrue: enterFullScreen }] = useBoolean() const playerProgress = ref('0%') let player: any const onClose = () => { player.pause() hidePlayer() playerProgress.value = '0%' } const onSwitch = (idx: number) => { const file: any = data.list[idx] if (file) { showPlayer() data.index = idx playerProgress.value = '0%' //上传时间较短,预览地址可能尚未转码成功。 let source = Date.now() - file.ctime > 15 * 60 * 1000 ? (file.preview_url || file.download_url) : file.download_url player.source = { type: data.type, title: file.name, sources: [{ src: source + "&t=" + Date.now(), size: 'Raw' }], } data.cur = { ...file } player.play().catch(() => { }) } } const onFullScreen = () => { enterFullScreen() player.fullscreen.enter() } const onDownload = () => { window.open(data.cur.download_url) } const onProgress = (e: any) => { const plyr = e.detail.plyr if (plyr.currentTime && plyr.duration) { playerProgress.value = Math.floor((100 * plyr.currentTime) / plyr.duration) + '%' } } const onError = (e: any) => { console.log(e) } const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent) onMounted(() => { player = new Plyr(el.value, { fullscreen: { enabled: true, fallback: true, iosNative: isIOS, container: undefined }, }) player.on('exitfullscreen', existFullScreen) player.on('timeupdate', onProgress) player.on('error', onError) }) onUnmounted(() => { removePlayer() }) watchEffect(() => { onSwitch(data.index) }) return () => (
播放列表
    {data.list.map((i: any, idx: number) => { return (
  • onSwitch(idx)} >
    {idx + 1}.
    {i.name}
    {i.ctimeDisplay}
  • ) })}
{data.cur.name}
{data.cur.ctimeDisplay}
) }, }) ================================================ FILE: packages/sharelist-manage/src/components/sider/index.less ================================================ .sider { display: flex; flex-direction: column; justify-content: space-between; height: 100%; width:64px; .menu{ .link{ color:var(--primary-text-color); position: relative; display: block; transition: all 0.3s; font-size:18px; text-align: center; padding: 1em 0; transition: all 0.3s; cursor: pointer; i{ color:currentColor; } &:hover{ color: var(--primary-background); } &.active{ background: var(--background-primary-1); color: var(--primary-background); &:after{ content:''; position: absolute; width:6px;height:6px; border-radius: 3px; background: var(--primary-background); // display: block; } } } } .logo { display: flex; justify-content: center; align-items: center; flex-direction: column; font-size: 12px; font-weight: bold; color: #fff; line-height: 1em; font-family: 'Source Code Pro'; text-shadow: 0 0 1px rgba(0, 0, 0, .1); background: var(--primary-background); width:40px; height:40px; border-radius: 20px; margin: 32px auto; text-align: center; } .sider-footer { font-size: 13px; color: var(--primary-text-color); margin: 0 auto; padding: 16px 0; display: flex; flex-direction: column; justify-content: center; button{ border-color:transparent; color:#999; } } } ================================================ FILE: packages/sharelist-manage/src/components/sider/index.tsx ================================================ import './index.less' import { ref, defineComponent, computed, watch, onMounted, onUnmounted, toRefs, reactive, watchEffect } from 'vue' import { DatabaseOutlined, AppstoreAddOutlined, SettingOutlined, ReloadOutlined, PoweroffOutlined } from '@ant-design/icons-vue' import { useRouter, useRoute, RouterLink, onBeforeRouteUpdate } from 'vue-router' import useConfirm from '@/hooks/useConfirm' import { useSetting } from '@/hooks/useSetting' import useDriveStore from '@/store/index' import { Dropdown, Menu } from 'ant-design-vue' export default defineComponent({ setup() { let driveStore = useDriveStore() onBeforeRouteUpdate((to, from) => { if (from.name == 'drive') { driveStore.savePath(from.fullPath) } }) const router = useRouter() const route = useRoute() const navToDrive = () => { router.push(driveStore.path || '/drive/folder') } const { signout, reload } = useSetting() const confirmSignout = useConfirm(signout, '确认', '确认退出?') const confirmReload = useConfirm(reload, '确认', '确认重新加载插件?') const curRoute = computed(() => route.name) const onAction = ({ key }: { key: string }) => { if (key == 'exit') { confirmSignout() } else if (key == 'reload') { confirmReload() } } return () =>
} }) ================================================ FILE: packages/sharelist-manage/src/components.d.ts ================================================ // generated by unplugin-vue-components // We suggest you to commit this file into source control // Read more: https://github.com/vuejs/core/pull/3399 import '@vue/runtime-core' export {} declare module '@vue/runtime-core' { export interface GlobalComponents { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } } ================================================ FILE: packages/sharelist-manage/src/config/api.ts ================================================ const api: Array = [ ['siginin', 'POST /signin'], ['list', 'GET /api/drive/path/:path'], ['setting', 'GET /api/setting', { token: true }], ['exportSetting', 'GET /api/setting?raw=true', { token: true }], ['saveSetting', 'POST /api/setting', { token: true }], ['config', 'GET /api/configs'], ['clearCache', 'PUT /api/cache/clear', { token: true }], ['reload', 'PUT /api/reload', { token: true }], // ['file', 'POST /api/drive/file/get', { token: true }], ['files', 'POST /api/drive/file/list', { token: true }], ['filePath', 'POST /api/drive/file/path', { token: true }], ['fileUpdate', 'POST /api/drive/file/update', { token: true }], ['fileDelete', 'POST /api/drive/file/delete', { token: true }], ['fileCreateUpload', 'POST /api/drive/file/create_upload', { token: true }], ['fileUpload', 'POST /api/drive/file/upload?task_id=:taskId', { token: true, contentType: 'stream' }], ['fileUploadCancel', 'GET /api/drive/file/cancel_upload/:$1?t=:$R', { token: true }], ['fileHashDownload', 'POST /api/drive/file/hash_save', { token: true }], ['mkdir', 'POST /api/drive/file/mkdir', { token: true }], ['diskDelete', 'POST /api/drive/disk/delete', { token: true }], ['tasks', 'GET /api/drive/tasks?t=:$R', { token: true }], ['task', 'GET /api/drive/task/transfer/:$1?t=:$R', { token: true }], ['resumeTask', 'PUT /api/drive/task/transfer/:$1/resume?t=:$R', { token: true }], ['pauseTask', 'PUT /api/drive/task/transfer/:$1/pause?t=:$R', { token: true }], ['removeTask', 'DELETE /api/drive/task/transfer/:$1', { token: true }], ['retryTask', 'PUT /api/drive/task/transfer/:$1/retry', { token: true }], ['remoteDownload', 'POST /api/drive/task/remote_download', { token: true }], ['pauseDownload', 'PUT /api/drive/task/remote_download/:$1/pause', { token: true }], ['resumeDownload', 'PUT /api/drive/task/remote_download/:$1/resume', { token: true }], ['removeDownloadTask', 'DELETE /api/drive/task/remote_download/:$1', { token: true }], ['fileMove', 'POST /api/drive/move', { token: true }], ['plugin', 'GET /api/plugin/:$1?t=:$R', { token: true }], ['savePlugin', 'POST /api/plugin', { token: true }], ['pluginStore', 'POST /api/plugin_store', { token: true }], ['removePlugin', 'DELETE /api/plugin/:$1?t=:$R', { token: true }], ['upgradePlugin', 'PUT /api/plugin/:$1/upgrade', { token: true }], ['installPlugin', 'POST /api/plugin_store/install', { token: true }], // ['parents', 'GET /api/drive/files/:fileId/parents', { token: true }], ] export default api ================================================ FILE: packages/sharelist-manage/src/config/setting.ts ================================================ export default { iconFontCN: 'https://at.alicdn.com/t/font_2637962_voky2m76mr.js', } ================================================ FILE: packages/sharelist-manage/src/hooks/useApi.ts ================================================ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import { effectScope, EffectScope, App, InjectionKey, getCurrentInstance, inject } from 'vue' export const apiSymbol = Symbol('api') as InjectionKey // export type APIItemGroup = Array<[string, string | ((...args: Array) => string), Record]> export type APIItem = [ name: string, url: string | ((...args: Array) => string), options?: { [key: string]: number | string | boolean | ((...rest: Array) => any) }, ] export type APICall = (...rest: Array) => Promise> export interface IUseApi { install?: (app: App) => void _e?: EffectScope _m?: any //Record } type RequestMethod = 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT' type ReqConfig = { url: string method: string data?: any params?: any responseType?: string token?: boolean headers?: any } axios.defaults.timeout = 60 * 1000 const service: AxiosInstance = axios.create() // http response 拦截器 service.interceptors.response.use( (response) => { return response.data }, (error) => { // 由接口返回的错误 if (error.response) { return { error: { code: error.response.status, message: error.response.statusText } } } else { log(`服务器错误!错误代码:${error}`) return { error: { code: error?.code || 500, message: '' } } } }, ) const log = (content: string, type = 'error'): void => { console.log(content) } export type ReqResponse = { error?: { code: number; message?: string; scope?: Record } [key: string]: any [key: number]: any } interface APIOptions { inScope?: boolean baseURL?: string onReq?: (d: Record, itemOption: Record) => void onRes?: (d: T) => void onError?: (e: Error) => void } // const qs = (d: Record) => Object.keys(d).map(i => `${i}=${encodeURI(d[i])}`).join('&') const urlReplace = (url: string, params: Record) => url.replace(/(?:\:)([\w\$]+)/g, ($0, $1) => { if ($1 in params) { return params[$1] } else { return '' } }) const convFormData = (data: any) => { const fd = new FormData() for (const i in data) { if (Array.isArray(data[i])) { const item = [] data[i].forEach((j: any, idx: number) => { fd.append(`${i}[${idx}]`, j) }) } else { fd.append(i, data[i]) } } return fd } export const useApi = (options?: APIOptions) => { if (globalApi) { return globalApi._m as any } const currentInstance = getCurrentInstance() const api = currentInstance && inject(apiSymbol) if (!api) { throw new Error( 'getActiveApi was called with no active api. Did you forget to install?\n' + '\tconst api = createApi()\n' + '\tapp.use(api)\n' + `This will fail in production.`, ) } return api._m as any } const globalApi: IUseApi = {} export const createApi = (apis: unknown, options?: APIOptions): IUseApi => { type a = typeof apis const pareKey: any = {} for (const i of apis as Array) { pareKey[i[0]] = 1 } const apiMap: Record = {} for (const i of apis as Array) { // pareKey[apis[0]] = apiMap[i[0]] = createRequest(i, options) } if (options?.inScope) { const scope = effectScope(true) const api: IUseApi = { install(app: App) { app.provide(apiSymbol, api) app.config.globalProperties.$api = api }, _e: scope, _m: apiMap, } return api } else { globalApi._m = apiMap return globalApi } } function createRequest(api: APIItem, defaultOptions?: APIOptions): APICall { return (...args: Array) => { const [name, url, options = {}] = api const reqUrl = typeof url === 'function' ? url(...args) : url let argsObj: Record = { $R: Math.random(), $T: Date.now(), } if (typeof args[0] == 'object') { argsObj = { ...argsObj, ...args[0] } } args.forEach((key, idx) => { argsObj['$' + (idx + 1)] = key }) const pairs = reqUrl.split(/\s/) const contentType = options.contentType || 'json' const params: any = { url: (defaultOptions?.baseURL || '') + urlReplace(pairs.slice(1).join(' '), argsObj), method: pairs[0] || 'GET', data: typeof args[0] == 'object' ? args[0] : {}, headers: options.header || {}, } if (options.params) { return params } // factory if (typeof params.data?.customRequest == 'function') { const ret: any = params.data.customRequest(params, options) delete params.data.customRequest if (ret) return ret // return (params, options) => { } } if (contentType == 'formdata') { params.headers['content-type'] = 'multipart/form-data' if (params.data) { params.data = convFormData(params.data) } } else if (contentType == 'stream') { params.headers['content-type'] = 'application/octet-stream' params.data = params.data.stream } else { params.headers['content-type'] = 'application/json' } defaultOptions?.onReq?.(params, options) return service.request(params as AxiosRequestConfig) } } ================================================ FILE: packages/sharelist-manage/src/hooks/useClipboard.ts ================================================ import { watch, onUnmounted, ref, Ref } from 'vue' type Type = 'file' | 'string' const watchOnce = (v: any, cb: any) => { const handler = watch( v, (nv) => { if (nv !== undefined) { handler() cb(nv) } }, { immediate: true }, ) return handler } export const useClipboard = (cb: (...rest: Array) => any, type: Type = 'string') => { const node: Ref = ref() const handler = (event: any) => { const items = (event.clipboardData || event.originalEvent.clipboardData).items const data: Array = [] for (let i = 0; i < items.length; i++) { if (items[i].kind === type) data.push(items[i]) } if (type == 'file') { cb(data.map((i) => i.getAsFile())) } else if (type == 'string') { cb(data.map((i) => i.getAsString())) } else { cb(data) } } const cancel = watchOnce(node, () => { node.value.addEventListener('paste', handler) }) onUnmounted(() => { cancel?.() node.value?.removeEventListener('paste', handler) }) return { node } } export default (el: Element, type: Type, cb: (...rest: Array) => any) => { const handler = (event: any) => { const items = (event.clipboardData || event.originalEvent.clipboardData).items console.log(items) const data: Array = [] for (let i = 0; i < items.length; i++) { if (items[i].kind === type) data.push(items[i]) } console.log(data, items.length, type, '<<') if (type == 'file') { cb(data.map((i) => i.getAsFile())) } else if (type == 'string') { cb(data.map((i) => i.getAsString())) } else { cb(data) } } el.addEventListener('paste', handler) return () => el.removeEventListener('paste', handler) } ================================================ FILE: packages/sharelist-manage/src/hooks/useConfirm.ts ================================================ import { Modal, message } from 'ant-design-vue' import { createVNode, VNode } from 'vue' import { ExclamationCircleOutlined } from '@ant-design/icons-vue' export default (fn: { (): any }, title = '确认', content = '') => (): { destroy(): void; update(p: any): void } => { const modal = Modal.confirm({ title, content, icon: createVNode(ExclamationCircleOutlined), onOk() { fn() }, }) return modal } type ConfirmOption = { title: string content: string success?: string icon?: VNode onSuccess?: () => void } export const useApiConfirm = ( fn: { (): Promise }, { title, content, success, icon, onSuccess }: ConfirmOption = { title: '确认', content: '' }, ): void => { Modal.confirm({ title, content, icon: icon || createVNode(ExclamationCircleOutlined), onOk() { return fn() .then((res) => { if (res.error) { message.error(res.error.message) } else { message.success(success || '操作完成') onSuccess?.() } return Promise.resolve() }) .catch((e) => { message.error(e.message) }) }, }) } ================================================ FILE: packages/sharelist-manage/src/hooks/useDirective.ts ================================================ import { VNode, withDirectives } from 'vue' const focus = { mounted: (el: any, { arg }: any) => { el.focus() if (arg == 'select') { el.select() } }, } const select = { mounted: (el: any) => { el.select() }, } export const useFocus = (node: VNode, select = false): VNode => withDirectives(node, [[focus, true, select ? 'select' : '']]) ================================================ FILE: packages/sharelist-manage/src/hooks/useDom.ts ================================================ import { ref, Ref } from 'vue' export const isDocumentVisible = (): boolean => { return document?.visibilityState !== 'hidden' } const docListeners: Array<() => any> = [] export const documentVisibility = ref(!document?.hidden) window.addEventListener( 'visibilitychange', () => { documentVisibility.value = !document.hidden if (documentVisibility.value) { for (let i = 0; i < docListeners.length; i++) { const listener = docListeners[i] listener() } } }, false, ) export const whenDocumentVisible = (listener: () => any): (() => void) => { docListeners.push(listener) return function unsubscribe() { const index = docListeners.indexOf(listener) docListeners.splice(index, 1) } } export const useDocumentVisibility = (): Ref => { return documentVisibility } export const useResize = () => { } ================================================ FILE: packages/sharelist-manage/src/hooks/useHooks.ts ================================================ import { ref, reactive, Ref, UnwrapRef, onMounted, onUnmounted, getCurrentInstance } from 'vue' export function safeOnMounted(hook: () => any): void { const instance = getCurrentInstance() if (instance?.isMounted || (instance as any)?._isMounted) { hook() } else { onMounted(hook) } } type ToggleValue = number | string | boolean | undefined export const useToggle = (defaultValue: ToggleValue = false, reverseValue: ToggleValue = undefined): any => { if (reverseValue === undefined) { reverseValue = !Boolean(defaultValue) } const state: Ref = ref(defaultValue) const toggle = () => { state.value = state.value === defaultValue ? reverseValue : defaultValue } const setLeft = () => (state.value = defaultValue) const setRight = () => (state.value = reverseValue) return { state, toggle, setLeft, setRight } } export const useBoolean = (defaultValue = false): any => { const state: Ref = ref(defaultValue) const toggle = () => { state.value = state.value === true ? false : true } const setTrue = () => (state.value = true) const setFalse = () => (state.value = false) return [state, { toggle, setTrue, setFalse }] } export const useObject = (defaultValue: Record): [any, (k?: Array) => void] => { const state = reactive(defaultValue) const clear = (excludes: Array = []): void => { Object.keys(state as Record) .filter((i) => !excludes.includes(i)) .forEach((key: string) => Reflect.deleteProperty(state as Record, key)) } return [state, clear] } export const useState = >(initialState: T = {} as T): [T, (state: T) => T, () => T] => { const state = reactive(initialState) const setState = (val: T, clear = false) => { Object.keys(val).forEach((key) => { Reflect.set(state, key, val[key]) }) return state as T } const clearState = () => { Object.keys(state).forEach((key) => Reflect.deleteProperty(state, key)) return state as T } return [state as T, setState, clearState] } const singleHook = new WeakMap() // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const useSingle = (hook: any, args: Array): any => { if (singleHook.has(hook)) { return singleHook.get(hook) } const res = hook.apply(hook, args) singleHook.set(hook, res) return res } // const useBoot = (cb: any) => { // useBoot.ready = true // if (useBoot.ready) { // cb() // } // } // const useWindowEvent = (event: string) => { // const handler = new Set() // const eventMap = new Map() // window.addEventListener(event, (event) => { // console.log('location: ' + document.location + ', state: ' + JSON.stringify(event.state)) // }) // const onMessage = (cb: any) => { // handler.add(cb) // } // const initListener = () => { // if (document) { // document.addEventListener('message', (e) => { // console.log(e) // }) // } // } // initListener() // return { // onMessage, // } // } type useTitleOptions = { restoreOnUnmount: boolean } export const useTitle = (title: string, options?: useTitleOptions): void => { const lastTitle = ref('') const run = () => { lastTitle.value = document.title document.title = title } safeOnMounted(() => { run() }) if (options?.restoreOnUnmount === true) { onUnmounted(() => { document.title = lastTitle.value }) } run() } export const delayToggle = (setLeft: () => any, setRight: () => any, timeout = 200) => { let handler: number | null return { true() { if (timeout === 0) { setLeft() } else { handler = setTimeout(() => { setLeft() }, timeout) } }, false() { if (handler) { clearTimeout(handler) handler = null } setRight() }, } } export const cancelablePromise = (origin: Promise): (() => void) => { let handler: (args: any) => void Promise.race([ origin, new Promise((resolve, reject) => { handler = resolve }), ]) return () => handler?.({}) } ================================================ FILE: packages/sharelist-manage/src/hooks/useLoad.ts ================================================ import { ref, Ref } from 'vue' export const useLoad = (cb: (() => Promise) | Promise): Ref => { const loading = ref(false) Promise.resolve(cb).then(() => { loading.value = true }) return loading } ================================================ FILE: packages/sharelist-manage/src/hooks/useLocalStorage.ts ================================================ import { Ref, ref, watch } from 'vue' export type LocalStateKey = string export function useLocalStorageState(key: LocalStateKey, defaultValue?: T | (() => T)): Ref { const raw = localStorage.getItem(key) if (raw) { try { defaultValue = JSON.parse(raw) } catch { // } } if (typeof defaultValue === 'function') { defaultValue = (defaultValue as () => T)() } const state = ref(defaultValue) as Ref const setState = () => { localStorage.setItem(key, JSON.stringify(state.value)) } watch( state, (nv) => { setState() }, { deep: true, immediate: false }, ) return state } ================================================ FILE: packages/sharelist-manage/src/hooks/useRequest.ts ================================================ import { reactive, ref, Ref, toRefs, watch, onUnmounted } from 'vue' export const useState = >(initialState: T = {} as T): [T, (state: T) => T, () => T] => { const state = reactive(initialState) const setState = (val: T, clear = false) => { Object.keys(val).forEach((key) => { Reflect.set(state, key, val[key]) }) return state as T } const clearState = () => { Object.keys(state).forEach((key) => Reflect.deleteProperty(state, key)) return state as T } return [state as T, setState, clearState] } const docListeners: Array<() => any> = [] export const documentVisibility = ref(!document?.hidden) window.addEventListener( 'visibilitychange', () => { documentVisibility.value = !document.hidden if (documentVisibility.value) { for (let i = 0; i < docListeners.length; i++) { const listener = docListeners[i] listener() } } }, false, ) export const whenDocumentVisible = (listener: () => any): (() => void) => { docListeners.push(listener) return function unsubscribe() { const index = docListeners.indexOf(listener) docListeners.splice(index, 1) } } type Service = (...args: P) => Promise interface RequestOptions { immediate?: boolean defaultParams?: P mutate?: (data?: T | ((oldData?: T) => T | undefined), disableRequest?: boolean) => void onBefore?: (param: P) => void onSuccess?: (data: T, param: P) => void onError?: (e: Error, param: P) => void onFinally?: (param: P, data?: T, e?: Error) => void loadingDelay?: number cacheKey?: string cacheTime?: number refreshDeps?: Array ready?: Ref pollingInterval?: number pollingWhenHidden?: boolean } type RequestState> = { loading: boolean params?: P data?: D error?: Error } interface RequestActions> { cancel: () => void refresh: () => void refreshAsync: () => Promise run: (...param: P) => void runAsync: (...param: P) => Promise mutate: (data: D) => void } interface RequestCore extends RequestActions { state: RequestState use: (plugins: Array>>) => void } type RequestResult = RequestActions & { [prop in keyof RequestState]: Ref[prop]> } interface PluginResult { before: (params: P) => | ({ stopNow?: boolean returnNow?: boolean } & Partial>) | void request: (service: Service, params: P) => { service?: Promise } success: (data: D, params: P) => void error: (e: Error, params: P) => void finally: (params: P, data?: D, e?: Error) => void cancel: () => void mutate: (data: D) => void } interface Plugin> { (requestInstance: RequestCore, options: RequestOptions): Partial> } type PickMethod = { [P in keyof C]: P extends M ? C[P] : never }[keyof C] const useRequestCore = >(service: Service, options: RequestOptions = {}) => { const [state, setState] = useState>({ loading: false, params: undefined, data: undefined, error: undefined, }) const plugins: Array>> = [] //const loading = ref(false) // data: Ref = ref() // params: Ref = ref(options.defaultParams) state.params = options.defaultParams as P let canceled = false const emit = ( type: T, ...rest: Parameters> ): ReturnType> => { // @ts-expect-error: Unreachable code error return { ...plugins.map((i) => i[type]?.(...rest)).filter(Boolean) } } const runAsync = async (...params: P) => { //params.value = args canceled = false const { returnNow, ...newState } = emit('before', params) || {} setState({ loading: true, params, ...newState, }) if (returnNow) { return Promise.resolve(state.data) } options.onBefore?.(params) try { let { service: servicePromise } = emit('request', service, params) if (!servicePromise) { servicePromise = service(...params) } const res = await servicePromise if (canceled) { return new Promise(() => {}) } setState({ data: res, error: undefined, loading: false, }) options.onSuccess?.(res, params) emit('success', res, params) options.onFinally?.(params, res) emit('finally', res, params) return res } catch (error: any) { if (canceled) { return new Promise(() => {}) } setState({ error, loading: false, }) options.onError?.(error, params) emit('error', error, params) options.onFinally?.(params, undefined, error) emit('finally', params, undefined, error) throw error } } const run = (...args: P) => { runAsync(...args).catch((e) => { if (!options.onError) { console.error(e) } }) } const mutate = (data: D) => { emit('mutate', data) state.data = data } const cancel = () => { canceled = true state.loading = false emit('cancel') } const use = (usePlugins: Array>>) => { plugins.push(...usePlugins) } const refresh = () => { // @ts-expect-error: Unreachable code error run(...(state.params || [])) } const refreshAsync = () => { // @ts-expect-error: Unreachable code error return runAsync(...(state.params || [])) } onUnmounted(cancel) return { state, use, run, mutate, cancel, refresh, runAsync, refreshAsync, } } export const useRequest = = Array>( service: Service, options: RequestOptions = {}, plugins?: Array>, ): RequestResult => { const pluginsFactory = [...(plugins || []), useDelay, usePolling, useAuto] as Array> const requestInstance = useRequestCore(service, options) requestInstance.use(pluginsFactory.map((plugin) => plugin(requestInstance, options))) const { loading, data, error, params } = toRefs(requestInstance.state) return { loading, error, data: data as Ref, params: params as Ref

, run: requestInstance.run, runAsync: requestInstance.runAsync, mutate: requestInstance.mutate, cancel: requestInstance.cancel, refresh: requestInstance.refresh, refreshAsync: requestInstance.refreshAsync, } } const usePolling: Plugin> = (request, { pollingInterval, pollingWhenHidden = true }) => { if (!pollingInterval) return {} let timer: number, docVisibleWatcher: () => void const stop = () => { if (timer) { clearTimeout(timer) } docVisibleWatcher?.() } return { before() { stop() }, finally() { if (!pollingWhenHidden && !documentVisibility.value) { docVisibleWatcher = whenDocumentVisible(request.refresh) return } timer = setTimeout(request.refresh, pollingInterval) }, cancel() { stop() }, } } const useDelay: Plugin> = (request, { loadingDelay = 0 }) => { if (!loadingDelay) return {} let timer: number const clear = () => { if (timer) { clearTimeout(timer) } } return { before() { clear() timer = setTimeout(() => { request.state.loading = true }, loadingDelay) return { loading: false, } }, finally() { clear() }, cancel() { clear() }, } } const useAuto: Plugin> = (request, { ready = ref(true), immediate = false, refreshDeps = [] }) => { if (refreshDeps) { watch(refreshDeps, () => { request.refresh() }) } if (immediate) { if (ready.value) request.refresh() else { watch(ready, (nv) => { if (nv) request.refresh() }) } } return { before() { if (!ready || !ready.value) { return { stopNow: true, } } }, } } ================================================ FILE: packages/sharelist-manage/src/hooks/useScroll.ts ================================================ import { ref, Ref, reactive, UnwrapRef } from 'vue' interface RequestOptions { immediate?: boolean isNoMore?: (data?: T) => boolean onSuccess?: (data: T) => void onError?: (e: Error) => void target?: Ref threshold?: number } interface actions { setNode(el: Element): void scrollTo(pos: number): void cancel: () => void checkScroll: () => void isScroll: Ref } type useScrollOption = { list: any[] [key: string]: any } export const useScroll = >( service: (args?: T) => Promise, options: RequestOptions = {}, ): actions => { let el: Element const isScroll = ref(false) const setNode = (v: Element) => { if (el) { cancel() } el = v el.addEventListener('scroll', onDomScroll) } const cancel = () => { el?.removeEventListener('scroll', onDomScroll) } const onDomScroll = throttle(() => { const { clientHeight, scrollTop, scrollHeight } = el isScroll.value = scrollTop > 0 if (scrollHeight - scrollTop - clientHeight < (options?.threshold || 100)) { service() } }, 200) const scrollTo = (v: number) => { if (el) { el.scrollTo({ top: v }) } } const checkScroll = () => { const { clientHeight, scrollTop, scrollHeight } = el if (clientHeight == scrollHeight) service() } return { setNode, cancel, scrollTo, isScroll, checkScroll, } } const throttle = function (fn: () => any, delay = 0) { let now: number, last = 0, timer: number | null, context: any, args: Array | null const later = function () { last = now fn.apply(context, <[]>args) timer = null context = args = null } const listen = function (this: any, ...rest: Array) { args = rest now = Date.now() //剩余时间 const remaining = delay - (now - last) if (remaining <= 0 || remaining > delay) { if (timer) { clearTimeout(timer) timer = null } last = now fn.apply(this, <[]>rest) if (!timer) context = args = null } else if (!timer) { timer = setTimeout(later, remaining) } } return listen } ================================================ FILE: packages/sharelist-manage/src/hooks/useSetting.ts ================================================ import { ref, Ref, reactive } from 'vue' import { useApi, ReqResponse } from '@/hooks/useApi' import { message } from 'ant-design-vue' import { useBoolean } from './useHooks' import { saveFile } from '../utils/format' import useStore from '@/store/index' type IUseSetting = { (): IUseSettingResult instance?: any } export interface IUseSettingResult { configFields: Ref> signout(): any reload(): any loginState: Ref isLoading: Ref getValue(key: string): any config: any setConfig(data: ISetting, msg?: string): Promise getConfig(token: string): void exportConfig(): void clearCache(): void getPlugin(id: string): Promise setPlugin(id: string, data: string): Promise removePlugin(id: string): Promise upgradePlugin(id: string): Promise reloadConfig(): void } export type ConfigFieldItem = { code: string label: string help?: string secret?: boolean type: 'string' | 'number' | 'boolean' | 'option' | 'array' | 'textarea' handler?: (...rest: any) => void validator?: (...rest: any) => boolean } type fieldGroup = { title: string children: Array } const fields: Array = [ { title: '常规', children: [ { code: 'title', label: '网站标题', type: 'string' }, { code: 'manage_path', label: '后台地址', type: 'string', help: '地址必须以 / 开头', validator: (val) => /^\//.test(val), handler: (nv: string, ov: string) => (location.href = location.href.replace(ov, nv)), }, { code: 'token', label: '后台密码', type: 'string', secret: true }, { code: 'proxy_enable', label: '全局代理', type: 'boolean' }, { code: 'index_enable', label: '目录浏览', type: 'boolean' }, { code: 'proxy_override_content_type', label: '代理时重写Content-Type', help: '此项是为了兼容某些挂载源,因返回内容的content type异常,导致无法在线播放的问题。', type: 'boolean', }, { code: 'anonymous_download_enable', label: '允许下载', type: 'boolean', help: '禁用此项后,预览也将不可用。', }, { code: 'expand_single_disk', label: '展开单一挂载盘', help: '只有一个挂载盘时,直接展示改挂载盘内容。', type: 'boolean', }, { code: 'per_page', label: '列表分页大小', help: '设置分页将自动禁用缓存。某些挂载源可能不支持自定义分页大小。0 表示不分页。', type: 'number', }, ], }, { title: '外观', children: [ { code: 'theme', label: '主题', type: 'option' }, { code: 'script', label: '自定义脚本', type: 'textarea' }, { code: 'style', label: '自定义样式', type: 'textarea' }, ], }, { title: '传输设置', children: [ { code: 'proxy_url', label: '代理地址', help: '当前支持 HTTP/HTTPS 代理。', type: 'string' }, { code: 'plugin_source', label: '插件源', type: 'option' }, ], }, { title: 'WebDAV', children: [ { code: 'webdav_enable', label: '启用 WebDAV', type: 'boolean' }, { code: 'webdav_path', label: 'WebDAV 路径', type: 'string' }, { code: 'webdav_proxy', label: 'WebDAV 代理', type: 'boolean' }, { code: 'webdav_user', label: 'WebDAV 用户名', type: 'string' }, { code: 'webdav_pass', label: 'WebDAV 密码', type: 'string' }, ], }, ] export const useSetting: IUseSetting = (): IUseSettingResult => { if (useSetting.instance) { return useSetting.instance } const request = useApi() const store = useStore() const config: ISetting = reactive({}) const [isLoading, { setFalse: hideLoading }] = useBoolean(true) const loginState = ref(0) const configFields: Ref> = ref(fields) const getConfig = (val: string) => { store.saveToken(val) request.setting().then((resp: ReqResponse) => { if (resp.error) { if (resp.error.code == 401) { store.saveToken('') loginState.value = 2 if (resp.error.message) { message.error(resp.error.message) } } } else { loginState.value = 1 updateSetting(resp.data as ISetting) } hideLoading() }) } const setConfig = (data: ISetting, msg = '已保存') => { // console.log(data) return request.saveSetting(data).then((resp: ReqResponse) => { if (resp.error) { message.error(resp.error.message || 'error') } else { updateSetting(resp.data as ISetting) // return Promise.resolve(true) message.success(msg) } }) } const reloadConfig = () => { getConfig(store.accessToken) } const updateSetting = (data: ISetting) => { for (const i in data) { config[i] = data[i] } configFields.value = [...fields, ...config.pluginConfig.map((i: any) => ({ title: i.name, children: i.config }))] } const getValue = (code: string) => { return config[code] } const signout = () => { store.removeToken() loginState.value = 2 Object.keys(config).forEach((key) => Reflect.deleteProperty(config, key)) } const reload = () => { request.reload().then((resp: any) => { // hidden() if (resp.error) { message.error(resp.error?.message) } else { message.success('操作成功') } }) } const clearCache = () => { // const hidden = message.loading('正在清除缓存', 0) request.clearCache().then((resp: any) => { // hidden() if (resp.error) { message.error(resp.error?.message) } else { message.success('操作成功') } }) } const exportConfig = () => { request.exportSetting().then((resp: any) => { // hidden() if (resp.error) { message.error(resp.error?.message) } else { saveFile(JSON.stringify(resp.data), 'config.json') } }) } const getPlugin = async (id: string): Promise => { return request.plugin(id).then((resp: any) => { if (resp.error) { message.error(resp.error?.message) } else { return resp.data } }) } const setPlugin = async (id: string, data: string): Promise => { const res = await request.savePlugin({ id, data }) if (res.error) { message.error(res.error?.message) throw new Error(res.error?.message) } else { message.success('保存成功') reloadConfig() } } const removePlugin = async (id: string): Promise => { const res = await request.removePlugin(id) if (res.error) { message.error(res.error?.message) //throw new Error(res.error?.message) } else { message.success('删除成功') reloadConfig() } } const upgradePlugin = async (id: string): Promise => { const res = await request.upgradePlugin(id) if (res.error) { message.error(res.error?.message) //throw new Error(res.error?.message) } else { message.success('更新成功') reloadConfig() } } if (!config.token && store.accessToken) { getConfig(store.accessToken) } else { loginState.value = 2 hideLoading() } return (useSetting.instance = { signout, reload, loginState, isLoading, getValue, configFields, config, setConfig, getConfig, exportConfig, clearCache, getPlugin, setPlugin, removePlugin, upgradePlugin, reloadConfig, }) } export const useConfig: IUseSetting = (): any => { const config: Record = reactive({}) const request = useApi() request.config().then((resp: ReqResponse) => { if (!resp.error) { for (const i in resp.data) { config[i] = resp.data[i] } } }) return { config } } ================================================ FILE: packages/sharelist-manage/src/hooks/useStore.ts ================================================ import { provide, inject, Ref, ref } from 'vue' type InjectType = 'root' | 'optional' export interface FunctionalStore { (): T key?: symbol root?: T } export const regStore = (store: FunctionalStore): T => { if (!store.key) { store.key = Symbol('functional store') } const depends = store() provide(store.key, depends) return depends } export const useStore = (store: FunctionalStore, type?: InjectType): any => { const key = store.key const root = store.root switch (type) { case 'optional': return inject(key) || store.root || null case 'root': if (!store.root) store.root = store() return store.root default: if (inject(key)) { return inject(key) } if (root) return store.root throw new Error(`状态钩子函数${store.name}未在上层组件通过调用useProvider提供`) } } export const useAsync = (cb: (() => Promise) | Promise): Ref => { const val = ref() Promise.resolve(typeof cb === 'function' ? cb() : cb).then((resp) => { val.value = resp }) return val } const jsbridge = { call: (name: string, param?: any, callback?: (value?: unknown) => void) => name, getGrayScaleValue: (v: any) => v, } type BridgeValue = { returnValue: string } type BridgeFunctions = 'getGrayScaleValue' export const useBridgeValue = ( fn: BridgeFunctions, params: P, ): { value: Ref; ready: Ref } => { const value: Ref = ref() const ready = ref(false) Promise.resolve(jsbridge[fn](params)).then((resp: T) => { value.value = resp ready.value = true }) return { value, ready } } type GrayParams = 'code1' | 'code2' type GrapValue = { returnValue: string } const { ready, value } = useBridgeValue('getGrayScaleValue', 'code') type ICdpSpaceInfo = { returnValue: string } function getCdpSpaceInfo(spaceCode: string): Promise { return new Promise((resolve) => { jsbridge.call('getCdpSpaceInfo', { spaceCode }, (resp: unknown) => { resolve(resp as ICdpSpaceInfo) }) }) } ================================================ FILE: packages/sharelist-manage/src/hooks/useUrlState.ts ================================================ import { useRouter, useRoute } from 'vue-router' import { reactive, watch } from 'vue' import { useState } from './useHooks' type initial = { params: Record query: Record } export default ({ params: initialParams, query: initialQuery }: initial = { params: {}, query: {} }): any => { const router = useRouter() const route = useRoute() const [params, updateParams] = useState({ ...initialParams, ...route.params, }) const [query, updateQuery] = useState({ ...initialQuery, ...route.query, }) const setQuery = (data: Record) => { router.push({ query: { ...route.query, ...updateQuery(data), }, }) } const setParams = (data: Record) => { router.push({ ...route.params, ...updateParams(data), }) } const setPath = (path: string) => { router.push(path) } watch(() => route.params, updateParams) watch(() => route.query, updateQuery) watch(query, (nv) => { console.log('>>>', nv) }) return { params, query, setQuery, setParams, setPath } } ================================================ FILE: packages/sharelist-manage/src/hooks/useWorker.ts ================================================ const fnCache = new WeakMap() type Fn = (...args: any[]) => any const WORKER_SCRIPT = () => { const methodsMap: Record = {} function invoke(name: string, params: unknown[], id: string) { try { if (!methodsMap[name]) { throw new Error('function ' + name + ' is not registered.') } const result = methodsMap[name].apply(null, params) Promise.resolve(result) .then(function onresolve(res) { self.postMessage( JSON.stringify({ data: res, name: name, id: id, }), ) }) .catch(function onerror(error) { throw error }) } catch (error) { throw error } } self.onmessage = function (e) { const data = JSON.parse(e.data) const type = data.type const name = data.name switch (type) { case 'add': methodsMap[name] = eval(data.code) break case 'remove': if (methodsMap[name]) { delete methodsMap[name] } break case 'clear': methodsMap = {} break case 'invoke': var params = data.params var id = data.id invoke(name, params, id) break } } } class WorkerFactory { worker: Worker constructor() { const url = URL.createObjectURL(new Blob([`(${WORKER_SCRIPT.toString()})()`])) this.worker = new Worker(url) } } export const useWorker = (fn: Fn) => { if (useWorker.instance) { return useWorker.instance } const name = fn.name const url = URL.createObjectURL(new Blob([WORKER_SCRIPT.toString()])) if (!fnCache.has(fn)) { const url = URL.createObjectURL(new Blob([__WORKER_SCRIPT__])) fnCache.set(fn, { name, }) } } ================================================ FILE: packages/sharelist-manage/src/hooks/utils.ts ================================================ import { getCurrentInstance, isRef, onMounted as vueOnMounted, onUnmounted as vueOnUnmounted, Ref } from 'vue' export const onMounted = (cb: () => any): void => { const instance = getCurrentInstance() if (instance) { if (instance?.isMounted) { cb() } else { vueOnMounted(cb) } } } export function onUnmounted(cb: () => any): void { if (getCurrentInstance()) { vueOnUnmounted(cb) } } ================================================ FILE: packages/sharelist-manage/src/index.html ================================================ sharelist

================================================ FILE: packages/sharelist-manage/src/main.ts ================================================ import { createApp, h } from 'vue' import App from './App' import router from './router' import { message, Spin, ConfigProvider } from 'ant-design-vue' import { createPinia } from 'pinia' import piniaPersist from 'pinia-plugin-persist' import apis from '@/config/api' import { createApi } from '@/hooks/useApi' import useStore from '@/store/index' // import 'ant-design-vue/dist/antd.variable.less' import '@/assets/style/index.less' import { LoadingOutlined } from '@ant-design/icons-vue' Spin.setDefaultIndicator({ indicator: h(LoadingOutlined, { style: { fontSize: '24px', }, spin: true, }), }) ConfigProvider.config({ theme: { primaryColor: 'rgb(100,58,218)', }, autoInsertSpaceInButton: false, }) const pinia = createPinia() pinia.use(piniaPersist) createApi(apis, { onReq(params, options) { if (options.token) { params.headers['Authorization'] = useStore().accessToken } }, }) createApp(App) .use(router) .use(pinia) // .use( // createApi(apis, { // onReq(params, options) { // if (options.token) { // params.headers['Authorization'] = useStore().accessToken // } // }, // }), // ) .provide('$message', message) .mount('#app') ================================================ FILE: packages/sharelist-manage/src/router/index.ts ================================================ import { createRouter, createWebHashHistory, createWebHistory, RouteRecordRaw, onBeforeRouteLeave } from 'vue-router' const routes: Array = [ { path: '/', component: () => import('../views/home'), redirect: '/drive/folder', children: [ { path: '/general', name: 'general', component: () => import('../views/general'), }, { path: '/drive/folder:path(.*)', name: 'drive', component: () => import('../views/disk'), /* children: [ { path: 'folder:path(.*)', name: 'file', component: () => import('../views/disk/files'), }, ], */ }, { path: '/plugin', name: 'plugin', component: () => import('../views/plugin'), }, ], }, ] const router = createRouter({ history: createWebHistory((window as any).MANAGE_BASE), routes, }) export default router ================================================ FILE: packages/sharelist-manage/src/store/index.ts ================================================ import { defineStore } from 'pinia' export default defineStore('sharelist_manage', { state: () => ({ accessToken: '', layout: 'list', theme: 'light', path: '', }), actions: { saveToken(token: string) { this.accessToken = token }, removeToken() { this.accessToken = '' }, setLayout(val: string) { this.layout = val }, savePath(input: string) { this.path = input }, }, persist: { enabled: true, strategies: [ { storage: localStorage, paths: ['layout'] }, { storage: sessionStorage, paths: ['accessToken'] }, ], }, }) ================================================ FILE: packages/sharelist-manage/src/types/IDrive.ts ================================================ type DrivePath = { protocol: string [key: string]: string | number } declare type DriverField = { key: string label: string value?: string | number | boolean options?: Array type?: 'string' | 'hidden' | 'number' | 'boolean' | 'list' help?: string fields?: Array required?: boolean } declare type IDrive = { name: string [key: string]: string | number } declare type IPlugin = { name: string id: string [key: string]: string | number } declare type DriverGuide = { key?: string label?: string fields: Array } declare type Driver = { protocol: string name?: string guide?: Array } declare type IFile = { id: string name: string size: number type: 'folder' | 'file' | 'drive' ctime: number mtime: number path: string extra?: Record [key: string]: any } ================================================ FILE: packages/sharelist-manage/src/types/shim.d.ts ================================================ declare module '*.vue' { import { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component } declare type ISetting = { title?: string index_enable?: boolean default_ignores?: Array [key: string]: any } ================================================ FILE: packages/sharelist-manage/src/types/source.d.ts ================================================ declare module '*.json' declare module '*.png' declare module '*.jpg' declare module 'vue-infinite-scroll' ================================================ FILE: packages/sharelist-manage/src/utils/format.ts ================================================ const EXT_IMAGE = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 'tif', 'svg', 'webp'] const EXT_AUDIO_SUPPORT = ['mp3', 'm4a', 'acc', 'wav', 'ogg', 'flac'] const EXT_VIDEO_SUPPORT = ['mp4', 'mpeg', '3gp', 'mkv'] export const getFileType = (v: string, type = 'file'): string => { if (type == 'folder' || type == 'drive') { return type } else { if (v) v = v.toLowerCase().split('.').pop() || '' if (['mp4', 'mpeg', 'wmv', 'webm', 'avi', 'rmvb', 'mov', 'mkv', 'f4v', 'flv'].includes(v)) { return 'video' } else if (['mp3', 'm4a', 'wav', 'wma', 'ape', 'flac', 'ogg'].includes(v)) { return 'audio' } else if (['doc', 'docx', 'wps'].includes(v)) { return 'word' } else if (['ppt', 'pptx'].includes(v)) { return 'ppt' } else if (['pdf'].includes(v)) { return 'pdf' } else if (['xls', 'xlsx', 'pdf', 'txt', 'yaml', 'yml', 'ini', 'cfg', 'xml', 'md'].includes(v)) { return 'doc' } else if (EXT_IMAGE.includes(v)) { return 'image' } else if ( [ 'js', 'ts', 'css', 'html', 'c', 'h', 'cpp', 'py', 'java', 'jsp', 'php', 'cs', 'go', 'swift', 'vue', 'rs', 'asp', 'sql', ].includes(v) ) { return 'code' } // else if (['zip', 'rar', '7z', 'tar', 'gz', 'gz2'].includes(v)) { // return 'archive' // } else { return 'file' } } } export const byte = (v: number, fixed?: number): string => { if (v === undefined || v === null || isNaN(v)) { return '-' } let lo = 0 while (v >= 1024) { v /= 1024 lo++ } const val = Math.floor(v * 100) / 100 return (fixed ? val.toFixed(fixed) : val) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'][lo] } const fix0 = (v: any) => (v > 9 ? v : '0' + v) export const time = (v: number): string => { if (!v) return '-' const date = new Date(v) const thisYear = new Date().getFullYear() let ret: string = fix0(date.getMonth() + 1) + '/' + fix0(date.getDay()) + ' ' + fix0(date.getHours()) + ':' + fix0(date.getMinutes()) if (thisYear != date.getFullYear()) { ret = date.getFullYear() + '/' + ret } return ret } export const formatFile = (file: IFile | Array): IFile | Array => { if (Array.isArray(file)) { file.forEach((i) => { i.ext = i.name.split('.').pop() i.mediaType = getFileType(i.name, i.type) if (i.ctime) i.ctimeDisplay = time(i.ctime) if (i.size) i.sizeDisplay = byte(i.size) }) } else { file.ext = file.name.split('.').pop() file.mediaType = getFileType(file.name, file.type) if (file.ctime) file.ctimeDisplay = time(file.ctime) if (file.size) file.sizeDisplay = byte(file.size) } return file } export const isMediaSupport = (name: string, type: 'audio' | 'video' | 'image'): boolean => { const ext: string = name.split('.').pop() || '' if (type == 'audio' && EXT_AUDIO_SUPPORT.includes(ext)) { return true } else if (type == 'video' && EXT_VIDEO_SUPPORT.includes(ext)) { return true } else if (type == 'image' && EXT_IMAGE.includes(ext)) { return true } return false } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const isType = (type: string) => (obj: any) => Object.prototype.toString.call(obj) === `[object ${type}]` export const isArray = isType('Array') export const isObject = isType('Object') export const isBlob = isType('Blob') export const isString = (v: string): boolean => typeof v == 'string' export const getBlob = (data: string, filename: string): Blob | undefined => { let blob try { blob = new Blob([data], { type: 'application/octet-stream' }) } catch (e) { /**/ } return blob } export const saveFile = (data: Blob | string, filename: string): void => { let blob: Blob | undefined if (isString(data as string)) { blob = getBlob(data as string, filename) } else { blob = data as Blob } if (blob && isBlob(blob)) { const URL = window.URL || window.webkitURL const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename const evt = document.createEvent('MouseEvents') evt.initEvent('click', false, false) // link.click() link.dispatchEvent(evt) URL.revokeObjectURL(link.href) } } ================================================ FILE: packages/sharelist-manage/src/views/disk/index.less ================================================ .drive{ display: flex; flex-direction: column; height:100vh; &.drive--lite{ height:auto; } .drive__header-assistant{ display: flex; justify-content: space-between; margin:0 24px 8px 32px; padding-bottom:12px; border-bottom: 1px solid var(--primary-hover-bg-color); font-size:13px; } .ant-checkbox-inner{ border-radius: 50%; box-sizing: border-box; } .ant-checkbox-indeterminate{ .ant-checkbox-inner{ background-color: var(--ant-primary-color); border-color: var(--ant-primary-color); &:after{ // background-color: var(--context-background); background-color: #fff; height:2px; } } } .drive__header{ display: flex; align-items: center; justify-content: space-between; color: rgba(0, 0, 0, .65); font-size: 22px; padding:32px 32px; z-index:1; // position: sticky; // top:0; .drive__header-back { margin-right: 1em; } // border-bottom:1px solid #f5f6f7; } // .drive-breadcrumb{ // overflow-x:auto ; // } .drive__actions{ flex:none; color: var(--context-1); .ant-badge-dot{ // background:var(--primary-background); } } .drive-search{ max-width:560px; width:90%; margin: auto; transform: translate(0,20vh); } .drive-body-wrap{ flex: 1 1 auto; overflow-y: auto; position: relative; } .drive-body{ flex:1 1 auto; .item{ display: flex; width:100%; justify-content: space-between; align-items: center; padding:12px 0; transition: all 0.3s; margin-bottom:1px; cursor: pointer; position: relative; color:var(--primary-text-color); &:hover{ background-color: var(--context-hover); .item-check{ opacity: 1; } } .item-check{ padding:0 8px; opacity: 0; transition:opacity 0.3s; } &.item--checked{ background-color: var(--primary-hover-theme-color); .item-check{ opacity: 1; } } &.item--disabled{ color: var(--context-3); cursor:not-allowed; pointer-events: none; } .item-icon__ext--md{ display: none; } .item-icon__ext--sm{ display: none; } .item-info{ padding:0 6px; color:rgba(0,0,0,.5); flex:0 0 auto; position:absolute; right:0; top:0; } .item-name{ flex: 1; min-width: 0; width:100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; // display: flex; position: relative; } .item-icon{ margin-right: 16px; flex:none; width:36px; position: relative; } .item-thumb{ width:100%; height:30px; background-repeat: no-repeat; background-size:cover; background-position: center center; flex:none; border-radius: 5px; } .item-icon__ext{ position: absolute; font-size: 12px; bottom: 5px; color:inherit; text-transform: uppercase; transform: scale(0.8); transform-origin:center; // width:42px; text-align: center; width:100%; font-weight: bold; // transform-origin: left; // left: 16px; } .item-meta{ flex:auto; display: flex; justify-content:space-between; align-items: center; overflow: hidden; color:inherit; } .item-ctime{ flex: 0 0 auto; // padding:0 24px; // width: 200px; color:var(--context-2); font-size: 12px; text-align: left; } .item-size{ // color:rgba(37, 38, 43, 0.72); color:var(--context-2); font-size: 12px; text-align: right; flex:0 0 80px; } } } .drive-body-mask{ position: absolute; top:0;left:0; width:100%; height:100%; opacity: 0; } .drive-body--padding{ padding:0 24px; } .drive-body.drive-body--grid{ display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 8px; .item{ padding-left:8px; padding-right:8px; flex-direction: column; .item-check{ position: absolute; right:0px; top:0px; } .item-icon{ width:auto; margin-right:0; margin-bottom:12px; } .item-meta{ flex-direction: column; // align-items: flex-start; align-items: center; width:100%; overflow: hidden; } .item-thumb{ height:64px; width:96px; } .item-name{ text-align: center; font-size:13px; } .item-ctime{ // display: none; color:var(--context-3); } .item-size{ display: none; } .item-icon__ext{ font-size:15px; } } } } .drive-toolbar-wrap{ position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); z-index: 1; transition: all .3s ease; opacity: 0; pointer-events: none; &.show{ opacity: 1; pointer-events: auto; bottom:32px; } } .drive-toolbar{ display: flex; align-items: center; padding: 8px 16px; border-radius: 10px; background: var(--dark-background); overflow: visible; user-select: none; box-shadow: 0 0 1px 1px rgb(28 28 32 / 5%), 0 8px 24px rgb(28 28 32 / 12%); color:#fff; border: 1px solid var(--divider-1); .drive-toolbar-item{ font-size:16px; padding:8px; color:#fff; margin:0 6px; border-radius: 6px; &:hover{ background-color: var(--color-white-4); } } } .drive-search{ .tips{ font-size: 12px; color:var(--context-2); margin-top: 1em; } .search-history__header{ font-size: 12px; color:var(--context); margin:1em 0; } .drive-search-input{ padding:8px; font-size:14px; } input{ border:none; background-color: transparent; flex:auto; // border-bottom: 1px solid var(--context-4); outline:none; } .search-history__body{ } .search-history__item{ display: flex; align-items: center; width:100%; justify-content: space-between; padding:0.8em 8px; border-radius: 3px; cursor: pointer; &:hover{ background-color:var(--context-hover); } color:var(--context_primary); } } // .badge ================================================ FILE: packages/sharelist-manage/src/views/disk/index.tsx ================================================ import { h, ref, Ref, defineComponent, watch, onMounted, onUnmounted, nextTick, toRef, computed, withModifiers } from 'vue' import useStore from '@/store/index' import { storeToRefs } from 'pinia' import { Spin, Modal, Dropdown, Popover, Menu, Badge, Checkbox, Tooltip, InputSearch, RadioGroup, Input } from 'ant-design-vue' import Icon from '@/components/icon' import useDisk, { IFile } from './partial/useDisk' import './index.less' import { isMediaSupport } from '@/utils/format' import MediaPlayer, { usePlayer } from '@/components/player' import Breadcrumb from './partial/breadcrumb' import Error from './partial/error' import { useSetting } from '@/hooks/useSetting' import { InfoCircleOutlined, LoadingOutlined, ScissorOutlined, EditOutlined, HddOutlined, FolderAddOutlined, EllipsisOutlined, DeleteOutlined, CloudSyncOutlined, PlusOutlined, CloudDownloadOutlined, DownloadOutlined, CloseCircleFilled, SwapOutlined, SortDescendingOutlined, CalendarOutlined, FieldBinaryOutlined, ArrowUpOutlined, ArrowDownOutlined, ClockCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue' import useConfirm, { useApiConfirm } from '@/hooks/useConfirm' import { useRoute, onBeforeRouteLeave } from 'vue-router' import { useApi } from '@/hooks/useApi' import { useActions } from './partial/action' import Task from './partial/task' import { useBoolean } from '@/hooks/useHooks' import { useLocalStorageState } from '@/hooks/useLocalStorage' import { Upload } from './partial/upload' import { useScroll } from '@/hooks/useScroll' import { useClipboard } from '@/hooks/useClipboard' import { showImage } from '@/components/image' export default defineComponent({ setup() { const { setConfig, clearCache, config } = useSetting() const searchHistory = useLocalStorageState>('search_history', []) const confirmClearCache = useConfirm(clearCache, '确认', '确认清除缓存?') const route = useRoute() const store = useStore() const diskIntance = useDisk() const { loading, files, error, setPath, paths, loadMore, diskConfig, current: currentDisk, setAuth, setSort, sortConfig, onUpdate } = diskIntance const { rename, move, remove, mkdir, flashDownload, uploadConfirm, addDisk, setDisk, remoteDownload, showInfo } = useActions(diskIntance) const { setPlayer } = usePlayer('player') const driveEl: Ref = ref() const { node: pasteEl } = useClipboard((files) => { let { id } = diskConfig.value let dest = '/' + [...paths.value].join('/') uploadConfirm(files, dest, id) }, 'file') const { scrollTo, setNode, cancel: cancelScroll, isScroll, checkScroll } = useScroll(loadMore) onMounted(() => { setNode(driveEl.value) onUpdate(() => { setTimeout(checkScroll, 0) }) }) onUnmounted(() => { cancelScroll?.() }) const onClick = (data: IFile) => { if (data.type == 'folder' || data.type == 'drive') { let target: any = { id: data.id } if (!data.path && !currentDisk.search) { target.path = currentDisk.path + '/' + data.name } setPath(target) } else if (data.type == 'file') { let mediaType = data.mediaType if ((data.mediaType == 'audio' || data.mediaType == 'video') && isMediaSupport(data.name, mediaType)) { const list: Array = files.value.filter((i: IFile) => isMediaSupport(i.name, mediaType)) setPlayer({ list, type: mediaType, index: list.findIndex((i: IFile) => i.id == data.id), }) } else if (data.mediaType == 'image') { const list: Array = files.value.filter((i: IFile) => isMediaSupport(i.name, 'image')) showImage(list.map(i => i.download_url), list.findIndex(i => i.id == data.id)) } else { window.open(data.download_url) } } } let currentFocusFile: Ref = ref() const onAction = ({ key }: { key: any }) => { targetBind.value = false if (key == 'info') { showInfo(currentFocusFile.value as IFile) } else if (key == 'mount_drive') { addDisk() } else if (key == 'mkdir') { mkdir(diskConfig.value) } else if (key == 'config') { let name = currentFocusFile.value?.name let idx = config.drives.findIndex((i: IDrive) => i.name == name) if (idx >= 0) { setDisk(config.drives[idx], idx) } } else if (key == 'rename') { rename(currentFocusFile.value as IFile) } else if (key == 'move') { move(currentFocusFile.value as IFile) } else if (key == 'delete') { remove(currentFocusFile.value as IFile) } else if (key == 'upload') { } else if (key == 'flash_upload') { flashDownload(diskConfig.value) } else if (key == 'remote_download') { remoteDownload(diskConfig.value) } } let isActionHide = true let targetBind = ref(false) const onContextChange = (visible: boolean) => { console.log('action:', visible) // isActionHide = true if (visible) { if (targetBind.value == false) { currentFocusFile.value = undefined } //isActionHide = false // onActionVisibleChange(visible) // currentFocusFile.value = undefined // } else { // currentFocusFile.value = undefined // targetBind = false targetBind.value = false } } const onHover = (i: IFile | null, e?: MouseEvent) => { //if (!actionVisible.value) { //willShow = !!i //if (willShow) { console.log('on hover') //已存在 if (currentFocusFile.value) { } currentFocusFile.value = i if (i) { targetBind.value = true } //} // onActionVisibleChange() //} /* if (i === null) { currentFocusFile.value = undefined } else { if (!actionVisible.value) { currentFocusFile.value = i } }*/ } const download = (file: IFile | Array) => { if (!Array.isArray(file)) { file = [file] } file.forEach((i: IFile) => { let a = document.createElement('a') let e = document.createEvent('MouseEvents') e.initEvent('click', false, false) a.href = i.download_url // 设置下载地址 a.download = i.name a.dispatchEvent(e) }) } const mainSlots = { overlay: () => { let isDriveLevel = diskConfig.value.isRoot // setTimeout(() => { // currentFocusFile.value = undefined // }, 0) // blank area if (currentFocusFile.value || targetBind.value) { if (isDriveLevel) { return 修改配置 删除 } else { return 信息 重命名 移动 删除 } } else { if (isDriveLevel) { return 挂载网盘 } else { return 新建文件夹 上传文件 上传文件夹 云端秒传 离线下载 } } } } watch( route, (nv) => { if (route.name == 'drive') { let target: any = { path: '/' + (route.params.path as string).replace(/^\//, '') } if (route.query.search) { target.search = route.query.search } setPath(target) scrollTo(0) // getFiles({ path: route.params.path as string }) } }, { immediate: true }, ) const onTagClick = ({ path, index }: any = {}) => { if (path == '/') { setPath({ path: '/' }) } else if (index < paths.value.length) { setPath({ path: '/' + paths.value.slice(0, index).join('/') }) } } const onSelect = (i: any) => { i.checked = !i.checked } const onUnselectAll = () => { files.value.forEach((i: any) => (i.checked = false)) } const onSelectAll = (e: { target: { checked: boolean } }) => { let val = e.target.checked files.value.forEach((i: any) => (i.checked = val)) } const selectState = computed(() => { let count = files.value.filter((i: IFile) => !!i.checked).length let containDir = files.value.some((i: IFile) => !!i.checked && i.type == 'folder') let containDrive = files.value.some((i: IFile) => !!i.checked && i.type == 'drive') let containFile = files.value.some((i: IFile) => !!i.checked && i.type == 'file') // 0 unselect, 1 partially selected, 2 select all, let state = count == 0 ? 0 : count == files.value.length ? 2 : 1 return { state, containDir, containDrive, count, total: files.value.length, containFile } }) const onToggleSearch = () => { const onSearch = (value: string) => { console.log(value) if (value) { // router.push({ path: router.currentRoute.value.path, query: { search: value } }) let historyRecords = [...searchHistory.value] let idx = historyRecords.indexOf(value) if (idx >= 0) { historyRecords.splice(idx, 1) } historyRecords.unshift(value) searchHistory.value = historyRecords setPath({ search: value, path: currentDisk.path, id: currentDisk.id }) modal.destroy() } } const remove = (value: string) => { let idx = searchHistory.value.indexOf(value) if (idx >= 0) { searchHistory.value.splice(idx, 1) } } const options: Array = [] const searchMode = diskConfig.value.search const tips = ref(searchMode == 1 ? '* 仅支持全局搜索' : searchMode == 2 ? '* 仅支持搜索当前目录(不含子目录)' : '') /* if (searchMode == 1) { options.push({ label: '所有文件', value: 'global' }) } if (searchMode > 1) { options.push({ label: '当前目录', value: 'local' }) } const searchType = ref(options[0]?.value) */ const modal = Modal.confirm({ class: 'fix-modal--alone', width: '560px', maskClosable: true, content: () => ( ), }) } const changeView = () => { let val = store.layout == 'list' ? 'grid' : 'list' store.setLayout(val) } return () => (
{(diskConfig.value.search) ? : null} {{ default: () => , content: () => }} {{ default: () => , overlay: () => ( 挂载网盘 新建文件夹 上传文件 上传文件夹 云端秒传 离线下载 ) }}
{ error.code === 0 ?
{selectState.value.state > 0 ? `已选 ${selectState.value.count} 项` : `共 ${files.value.length} 项`}
{{ default: () => 按{sortConfig.value.key == 'name' ? '名称' : sortConfig.value.key == 'size' ? '文件大小' : '修改时间'}{sortConfig.value.type == 'asc' ? '升序' : '降序'}, overlay: () => setSort(key)}>
名称
{sortConfig.value.key == 'name' ? (sortConfig.value.type == 'asc' ? : ) : null}
修改时间
{sortConfig.value.key == 'mtime' ? (sortConfig.value.type == 'asc' ? : ) : null}
文件大小
{sortConfig.value.key == 'size' ? (sortConfig.value.type == 'asc' ? : ) : null}
}}
{{ title: '切换视图', default: () => }}
: null }
0 ? 'show' : null]}>
{ !selectState.value.containDir && selectState.value.containFile ? {{ title: '下载', default: () => download(files.value.filter((i: any) => (i.checked)))} class="drive-toolbar-item" /> }} : null } {{ title: '删除', default: () => remove(files.value.filter((i: any) => (i.checked)))} class="drive-toolbar-item danger-aciton--hover" /> }} { selectState.value.count == 1 ? {{ title: '重命名', default: () => rename(files.value.filter((i: any) => (i.checked))[0])} /> }} : null } { !selectState.value.containDrive ? {{ title: '移动', default: () => move(files.value.filter((i: any) => (i.checked)))} class="drive-toolbar-item" /> }} : null } { selectState.value.count == 1 ? {{ title: '信息', default: () => showInfo(files.value.filter((i: any) => (i.checked))[0])} /> }} : null } {{ title: '取消多选', default: () => }}
) }, }) ================================================ FILE: packages/sharelist-manage/src/views/disk/partial/action/index.less ================================================ .file-item{ padding-right:8px; overflow-x: hidden; .ant-checkbox-inner{ border-radius: 50%; box-sizing: border-box; } .file-item__rm{ opacity: 0; transition: opacity 0.3s; padding:8px; cursor: pointer; &:hover{ color:var(--color-red); } } &:hover{ background-color: var(--primary-hover-bg-color); .file-item__rm{ opacity: 1; } } } .setting{ .setting__item{ // display: flex; // align-items: center; padding:8px 0; margin-top:8px; .setting__item-label{ flex:none; // width:100px; font-size:12px; color:var(--context-2); display: block; margin-bottom:0.6em; } } &.small{ font-size:12px; } } .file-detail__item{ padding:8px 16px; .file-detail__item-label{ font-size:12px; color:var(--context-3); } .file-detail__item-content{ font-size:14px; color:var(--primary-text-color); } } ================================================ FILE: packages/sharelist-manage/src/views/disk/partial/action/index.tsx ================================================ import { defineComponent, withDirectives, ref, getCurrentInstance, Ref, computed, reactive } from "vue"; import { Input, Modal, message, Alert, Checkbox, InputNumber, Radio, Tooltip, Textarea } from 'ant-design-vue' import useDisk from '../useDisk' import { EditOutlined, ScissorOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownloadOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue' import { useApi } from "@/hooks/useApi"; import { Meta, Tree } from '../meta' import { useApiConfirm } from '@/hooks/useConfirm' import { byte, getFileType, time } from '@/utils/format' import { useUpload } from '../upload' import Modifier from '../modifier' import { useSetting } from '@/hooks/useSetting' import './index.less' import { useFocus } from '@/hooks/useDirective' // import { IUseSettingResult } from '../useDisk' interface IFileError extends IFile { errorMessage?: string } const FileErrorList = defineComponent({ props: { files: Array }, emits: ['change'], setup(props, { emit }) { let unchecked: Ref | any> = ref(props.files?.map(i => true)) const toggle = (idx: number) => { unchecked.value[idx] = !unchecked.value[idx] emit('change', unchecked.value.reduce((t: Array, c: boolean, idx: number) => c ? t.concat(idx) : t, [])) } return () =>
} }) const FileSelect = defineComponent({ props: { files: Array }, emits: ['change'], setup(props, { emit }) { let unchecked: Ref | any> = ref(props.files?.map(i => true)) const toggle = (idx: number) => { unchecked.value[idx] = !unchecked.value[idx] emit('change', unchecked.value.reduce((t: Array, c: boolean, idx: number) => c ? t.concat(idx) : t, [])) } return () =>
} }) export const useActions = (diskIntance: any) => { let { mutate, setAuth, current, setPath, reload } = diskIntance const appContext = getCurrentInstance()?.appContext; let request = useApi() const { setConfig, clearCache, config } = useSetting() const rename = (i: IFile) => { let name = i.name const onChange = (e: any) => { name = e.target.value } const onSave = async () => { if (name != i.name) { let res = await request.fileUpdate({ id: i.id, name }) // console.log(res) if (res.error) { message.error(res.error.message) throw new Error() } else { i.name = name mutate(i) modal.destroy() } } } const modal = Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, appContext, autoFocusButton: null, title:
重命名
, content: () => (
{useFocus(, true)}
), onOk: onSave, }) } const move = (file: IFile | Array) => { let dest: String let files: Array = Array.isArray(file) ? file : [file] let srcPath = diskIntance.current.path let isTransfer = ref(false) let threadNum = ref(1) let buttonProps = reactive({ disabled: true }) let count = files.length let isSingleTask = count == 1 let error: Array = [] let current = ref(0) let cancelFlag = ref(false) let key = `${Math.random()}` let targetMultiThreading = ref(false) // 排除 当前目录 及 子目录 let excludePath = [ srcPath, ...files.map(i => i.id) ] const onSelect = (e: IFile, targetDiskConfig: any) => { let autoExpand = config.expand_single_disk let diskCount = config.drives.length let destPath = e.path let destIsRoot = destPath == '/' // 根目录是磁盘列表 let rootIsDiskList = (!autoExpand || diskCount > 1) buttonProps.disabled = destPath == srcPath || (destIsRoot && rootIsDiskList) isTransfer.value = rootIsDiskList && srcPath.split('/')[1] != destPath.split('/')[1] && !destIsRoot dest = e.id targetMultiThreading.value = targetDiskConfig.multiThreading } const execMove = async () => { message.loading({ content: `正在${isTransfer.value ? '创建迁移任务' : '移动'}`, key, duration: 0 }); for (let i of files as Array) { if (cancelFlag.value) return message.loading({ content: `[${current.value + 1}/${count}]` + '正在移动:' + i.name, key, duration: 0 }); let res = await request.fileUpdate({ id: i.id, dest, threadNum: threadNum.value }) if (res.error) { i.error = res.error.message error.push(i.id) } else { if (!isTransfer.value) mutate(i, true) } current.value++ } let errorFiles = files.filter((i: IFile) => error.includes(i.id)) if (errorFiles.length) { if (isSingleTask) { message.error({ content: (files as Array)[0].error, key, duration: 1 }); } else { message.success({ content: '操作完成', key, duration: 0.01 }); errorModal(errorFiles) } } else { message.success({ content: isTransfer.value ? '迁移任务创建成功' : '操作完成', key, duration: 1 }); } } const errorModal = (errorFiles: Array) => { let retryList: Array = [] Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, title: () =>
移动
, okText: '重试', content: () => (
), onOk: () => { if (retryList.length) { files = (files as Array).filter((_, idx: number) => retryList.includes(idx)) //move((files as Array).filter((_, idx: number) => retryList.includes(idx))) execMove() } }, }) } const modal = Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, title: () =>
移动
, content: () => (
), okButtonProps: buttonProps, onOk: () => { execMove() }, }) } const mkdir = (i: IFile) => { let name = '新建文件夹' const onChange = (e: any) => { name = e.target.value } const onSave = async () => { if (name) { let res = await request.mkdir({ id: i.id, name }) if (res.error) { message.error(res.error.message) } else { mutate(res, 1) modal.destroy() } } } const modal = Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, autoFocusButton: null, title: () =>
新建文件夹
, content: () => ( ), onOk: onSave, }) } const flashDownload = (i: any) => { let options = typeof i.hashUpload == 'string' ? { type: i.hashUpload } : i.hashUpload let params = { name: '', hash: '', size: 0 } const onSave = async () => { if (options.size && !params.size) return if (!params.hash || !params.name) return let res = await request.fileHashDownload({ id: i.id, ...params }) // console.log(res) if (res.error) { message.error(res.error.message) throw new Error() } else { message.success('秒传成功') // mutate(res) modal.destroy() reload() } } const modal = Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, autoFocusButton: null, title: () =>
云端秒传
, content: () => (
), onOk: onSave, }) } const uploadConfirm = (files: Array, dest: string, id: string) => { const { create } = useUpload() let checked: Array = files.map((_, idx: number) => idx) const onChecked = (ids: Array) => { checked = ids } const ensure = () => { const checkedFiles = checked.map((idx: number) => files[idx]) create(checkedFiles, 'md5', dest, id) message.success('任务创建成功') } const data = files.map((i, idx) => ({ idx: idx, id: i.name, size: i.size, name: i.name, ctime: i.lastModified, ctimeDisplay: time(i.lastModified), sizeDisplay: byte(i.size), iconType: getFileType(i.name), checked: true })) Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, title: () =>
文件上传
, content: , onOk: ensure, }) } const remove = (files: IFile | Array) => { if (!Array.isArray(files)) { files = [files] } if (files.some(i => i.type == 'drive')) { removeDisk(files.filter(i => i.type == 'drive')) return } let count = files.length const error: Array = [] const current = ref(0) const cancelFlag = ref(false) const key = '' + Math.random() const modal = Modal.confirm({ title: '删除文件', content: `确认删除 ${count == 1 ? files[0].name : `${count} 项`}?`, onOk() { execRemove() }, onCancel() { cancelFlag.value = true } }) const execRemove = async () => { message.loading({ content: '正在删除', key, duration: 0 }); for (let i of files as Array) { if (cancelFlag.value) return message.loading({ content: `[${current.value + 1}/${count}]` + '正在删除:' + i.name, key, duration: 0 }); let res = await request.fileDelete({ id: i.id }) if (res.error) { error.push(i.id) } else { mutate(i, true) } current.value++ } let errorFiles = files.filter((i: IFile) => error.includes(i.id)) if (errorFiles.length) { return modal.update({ content: () =>
以下文件删除失败
}) } else { message.success({ content: '已成功删除', key, duration: 1 }); } } } const setDisk = (data: IDrive, idx = -1, msg: string = '修改成功') => { const updateData = async (modifyData: IDrive) => { const saveData = [...config.drives] // console.log(saveData, idx) if (idx == -1) { saveData.push(modifyData) } else { saveData[idx] = modifyData } await setConfig({ drives: saveData }, msg) // update files if (!current.path || current.path == '/') { setPath({ path: '/' }, true) } modal.destroy() } const modal = Modal.confirm({ class: 'fix-modal--alone', width: '720px', closable: true, content: (
), onOk: () => { }, }) } const addDisk = () => { setDisk( { name: '', protocol: '', }, -1, '创建成功' ) } const removeDisk = async (files: IFile | Array) => { if (!Array.isArray(files)) { files = [files] } const execRemove = () => { const saveData = [...config.drives] for (let i of files as Array) { let idx = saveData.findIndex(j => j.id == i.extra?.config_id) if (idx) { saveData.splice(idx, 1) } } setConfig({ drives: saveData }, '删除成功') if (!current.path || current.path == '/') { setPath({ path: '/' }, true) } } //await request.removeDrive({drive: files.map(i => i.id) }) let count = files.length Modal.confirm({ title: '删除挂载盘', content: `确认删除 ${count == 1 ? files[0].name : `${count} 项`}?`, onOk() { execRemove() }, onCancel() { } }) } const remoteDownload = (i: any) => { let url = ref('') let threadNum = ref(1) const onSave = async () => { if (url.value) { let res = await request.fileUpdate({ dest: i.id, id: url.value, threadNum: threadNum.value }) if (res.error) { message.error(res.error.message) throw new Error() } else { message.success('任务创建成功') modal.destroy() } } } const modal = Modal.confirm({ class: 'pure-modal', width: '500px', closable: true, autoFocusButton: null, title: () =>
离线下载
, content: () => (