Full Code of reruin/sharelist for AI

master 22c46c56e798 cached
226 files
589.4 KB
203.9k tokens
285 symbols
1 requests
Download .txt
Showing preview only (647K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>ShareList Docs</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
  <meta name="description" content="Description">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css"> -->
  <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/themes/vue.css">
  <style type="text/css">
  .sidebar ul li.active>a {
    border-right: none;
  }
  </style>
</head>

<body>
  <div id="app"></div>
  <script>
  window.$docsify = {
    name: 'ShareList',
    repo: 'https://github.com/reruin/sharelist',
    basePath:
    'https://raw.githubusercontent.com/reruin/sharelist/master/docs/',
    loadSidebar: true,
    loadNavbar: true,
    autoHeader: true,
    subMaxLevel: 2,
    search: {
      placeholder: {
        '/zh-cn/': '搜索',
        '/en/': 'Search',
      },
      noData: {
        '/zh-cn/': '找不到结果',
        '/en/': 'noData',
      },
    },
    nameLink: {
      '/zh-cn/': '#/zh-cn/',
      '/en/': '#/en/',
    },
  }
  </script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
</body>

</html>

================================================
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\<File\>|目录内容集合
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\<string\>|可用主题
data.index_enable|boolean|是否启用目录索引
data.ignores|Array\<string\>|忽略路径
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, `<div class="guide">
  <h3>挂载 Aliyun Drive</h3>
  <p>参考<a href="https://media.cooluc.com/decode_token/">此链接</a></p>
  </div>`)
}

================================================
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 => `<div><div class="label">${i}:</div>${ret[i]}</div>`).join('<br />')
        render(ctx, `
        <div class="guide">
          ${cnt}
        </div >
      `)
      }
    }
    else if (ctx.query.error) {
      ctx.body = req.query.error
    }
  } else {
    render(ctx, `
      <div class="guide">
        <h3>挂载Baidu Netdisk</h3>
        <p>1. 前往 <a target="_blank" style="margin-right:5px;cursor:pointer;" href="https://pan.baidu.com/union/console/createapp">Baidu网盘开发平台</a> 注册应用获取 API KEY" 和 SECRET KEY,注册类别 请选择为【软件】。<br />2. 前往 <a target="_blank" style="margin-right:5px;cursor:pointer;" href="https://pan.baidu.com/union/console/applist">应用详情->安全设置</a> 将【OAuth授权回调页】设置为: </p>
        <p><a target="_blank" href="https://github.com/reruin/reruin.github.io/blob/master/sharelist/redirect.html" style="font-size:12px;margin-right:5px;color:#337ab7;">https://reruin.github.io/sharelist/redirect.html</a></p>
  
        <form class="form-horizontal" method="post">
          <input type="hidden" name="act" value="install" />
          <div class="form-item" style="font-size:12px;"><label class="flex"><input disabled checked="true" name="custom" id="j_custom" type="checkbox"> 使用自己的应用ID 和 应用机密</label>,请遵循 <a href="https://pan.baidu.com/union/document/protocol" target="_blank">使用协议</a>。</div>
          <div class="form-item"><input id="j_client_id" class="sl-input" type="text" name="client_id" placeholder="应用ID / AppKey" /></div>
          <div class="form-item"><input id="j_client_secret" class="sl-input" type="text" name="client_secret" placeholder="应用机密 / SecretKey" /></div>
          <button class="sl-button btn-primary" id="signin" type="submit">验证 / Verify</button>
        </form>
        <script>
        function toggleCustom(){
          var checked = $('#j_custom').prop("checked")
          if( checked ){
            $('.custom').show()
          }else{
            $('.custom').hide()
          }
        }
        $(function(){
          $('#j_custom').on('change' , function(){
            toggleCustom()
          })
        })
        </script>
      </div>
    `)
  }
}

================================================
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 => `<div><div class="label">${i}:</div>${ret[i]}</div>`).join('<br />')
      render(ctx, `
      <div class="guide">
        ${cnt}
      </div>`)
    }
  } 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 => `<div><div class="label">${i}:</div>${credentials[i]}</div>`).join('<br />')
        render(ctx, `
        <div class="guide">
          ${cnt}
        </div>`)
      }
    }
    else if (ctx.query.error) {
      ctx.body = req.query.error
    }
  } else {
    render(ctx, `
    <div class="guide">
      <h3>挂载GoogleDrive</h3>
      <p>1. 请参考 <a target="_blank" style="font-size:12px;margin-right:5px;color:#337ab7;" href="https://developers.google.com/workspace/guides/create-project">此链接</a>创建项目,获取 Client ID / Client Secret。你也可以使用 rclone 的这组,但是有一定几率触发 Rate Limit Exceeded </p>
      <p>Client ID:202264815644.apps.googleusercontent.com</p>
      <p>Client Secret:X4Z3ca8xfWDb1Voo-F9a7ZxJ</p>
      <p>2. 在下方填写Client ID / Client Secret后,<a target="_blank" style="font-size:12px;margin-right:5px;color:#337ab7;" id="j_code_link"  onclick="directToCodeUrl(this)">点击获取验证code</a>,若出现[Google hasn't verified this app],请展开Advanced,点击[Go to Quickstart (unsafe)]。 </p>

      <form class="form-horizontal"  method="post">
        <input type="hidden" name="act" value="install" />
        <input type="hidden" name="redirect_uri" id="j_direct_uri" value="urn:ietf:wg:oauth:2.0:oob" />
        <div class="form-item"><input id="j_client_id" class="sl-input" type="text" name="client_id" placeholder="应用ID / Client ID" /></div>
        <div class="form-item"><input id="j_client_secret" class="sl-input" type="text" name="client_secret" placeholder="应用机密 / Client Secret" /></div>
        <div class="form-item"><input id="j_code" class="sl-input" type="text" name="code" placeholder="code" /></div>
        <button class="sl-button btn-primary" id="signin" type="submit">验证 / Verify</button>
      </form>
      <script>
        var codeUrl;
        function readFile(input){
          if (window.FileReader) {
            var file = input.files[0];
            filename = file.name.split(".")[0];
            var reader = new FileReader();
            reader.onload = function() {
              try{
                var d = JSON.parse( this.result );
                var data = Object.values(d)[0]
                var client_id = data.client_id;
                var client_secret = data.client_secret;
                var redirect_uris = data.redirect_uris;

                var hit = redirect_uris.find(function(i){
                  return  i.indexOf('urn:ietf') == 0
                })

                document.querySelector('#j_client_id').value = client_id;
                document.querySelector('#j_client_secret').value = client_secret;

                if(hit){
                  codeUrl = "https://accounts.google.com/o/oauth2/auth?client_id="+client_id+"&redirect_uri="+hit+"&response_type=code&access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&approval_prompt=auto";

                  document.querySelector('#j_direct_uri').value = hit;
                }
                
              }catch(e){
                console.log(e)
                alert('文件无效')
              }
            }
            reader.readAsText(file,"UTF-8");
          }
        }

        function directToCodeUrl(el){
          var client_id = document.querySelector('#j_client_id').value ;
          var client_secret = document.querySelector('#j_client_secret').value;
          if(client_id && client_secret){
            var codeUrl = "https://accounts.google.com/o/oauth2/auth?client_id="+client_id+"&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&approval_prompt=auto";

            window.open(codeUrl)
          }else{
            alert('请输入Client ID / Client Secret')
          }
        }
      </script>
    </div> `)
  }
}

================================================
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<br/>require sharepoint site'
      }
    }

    if (err) {
      render(ctx, `
        <div class="guide">
          '<h3>挂载向导</h3>'
          <p style="font-size:12px;">${err}<br /></p>
          <p><a style="font-size:12px;cursor:pointer;" onclick="location.href=location.pathname">点击重新开始</a></p>
        </div >
      `)
    }
    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 => `<div><div class="label">${i}:</div>${ret[i]}</div>`).join('<br />')
        render(ctx, `
        <div class="guide">
          ${cnt}
        </div >
      `)
      }
    }
    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(`<option value="${key}" data-sharepoint="${value[4] || ''}" data-portal="${value[2] || ''}" ${key == 'COM' ? 'selected' : ''}>${value[3]}</option>`)
    }

    render(ctx, `<div class="guide">
      <h3>OneDrive 挂载向导</h3>
      
      <form class="form-horizontal" method="post">
        <div class="l-center" style="font-size:13px;margin-bottom:24px;">
          <label><input type="radio" name="type" value="onedrive" checked /> OneDrive 挂载</label>
          <label><input type="radio" name="type" value="sharepoint" /> SharePoint 挂载</label>
          <label><input type="radio" name="type" value="sharelink" /> SharePoint 分享链接挂载</label>
        </div>
        <input type="hidden" name="act" value="install" />

        <div class="form-body">
          <div class="form-item tab tab-onedrive tab-auto tab-sharepoint">
            <select id="j_zone" name="zone">
              ${zone.join('')}
            </select>
          </div>
          <div class="tab tab-sharepoint">
            <p>前往 <a style="margin-right:5px;cursor:pointer;" id="j_portal_office">office365</a>,点击应用 sharepoint,创建一个网站,将URL地址填入下方输入框中。</p>
            <input class="sl-input zone_change_placeholder" type="text" name="sharepoint_site" value="" placeholder="URL https://xxxxxx.sharepoint.com/sites(teams)/xxxxxx" />
          </div>
          <div class="tab tab-sharepoint tab-onedrive">
            <div class="form-item" style="font-size:12px;"><label><input name="custom" id="j_custom" type="checkbox"> 使用自己的应用ID 和 应用机密</label></div>
            <div class="tab-custom">
              <p>前往 <a style="margin-right:5px;cursor:pointer;" id="j_portal">Azure管理后台</a> 注册应用获取 应用ID 和 应用机密。重定向 URI 请设置为: </p>
              <p><a target="_blank" href="https://github.com/reruin/reruin.github.io/blob/master/sharelist/redirect.html" style="font-size:12px;margin-right:5px;color:#337ab7;">https://reruin.github.io/sharelist/redirect.html</a></p>
              <div class="form-item"><input class="sl-input" type="text" name="client_id" value="" placeholder="应用ID / app_id" /></div>
              <div class="form-item"><input class="sl-input" type="text" name="client_secret" value="" placeholder="应用机密 / app_secret" /></div>
              <div class="form-item"><input class="sl-input" type="text" name="tenant_id" value="" placeholder="租户ID / tenant_id (多租户可选)" /></div>

            </div>
          </div>
          

          <div class="form-item tab tab-sharelink"><input class="sl-input" type="text" name="share_url" value="" placeholder="URL https://xxxx.sharepoint.com/:f:/g/personal/xxxxxxxx/mmmmmmmmm?e=XXXX" /></div>

        </div>
        <button class="sl-button btn-primary" id="signin" type="submit">验证</button>
      </form>
      
    </div>
    <script>
      function toggleType(type){
        // $('.tab.tab-'+type).fadeIn().siblings('.tab').fadeOut() 
        $('.tab').hide()
        $('.tab.tab-'+type).fadeIn(150)
        toggleCustom()

      }

      function toggleCustom(){
        var checked = $('#j_custom').prop("checked")
        if( checked ){
          $('.tab-custom').show()
        }else{
          $('.tab-custom').hide()
        }
      }

      $(function(){
        $('input:radio[name=type]').on('change', function() {
          toggleType(this.value)
        });

        $('#j_portal').on('click' , function(){
          var option = $("#j_zone option:selected")
          var portal = option.attr('data-portal') + '/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade';

          window.open(portal)
        })

        $('#j_portal_office').on('click' , function(){
          var option = $("#j_zone option:selected")
          var portal = option.attr('data-sharepoint');
          if( portal ){
            window.open(portal)
          }else{
            alert('暂不支持当前地域')
          }
        })

        $('#j_type').on('change' , function(){
          $('.form-item').hide()
          $('.form-item.tab-'+type).fadeIn(150)
        })

        $('#j_custom').on('change' , function(){
          toggleCustom()
        })

        $('#j_zone').on('change' , function(){
          let zone = $(this).val().toLowerCase()
          $('input.zone_change_placeholder').each(function(){
            var tip = $(this).attr('placeholder')
            $(this).attr('placeholder' , tip.replace(/sharepoint\.[a-z]+/,'sharepoint.'+zone))
          })
        })
        
        toggleType($('input:radio[name=type]:checked').val())
      })


    </script>
  `)
  }
}

================================================
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 = `<!DOCTYPE html><html><head><title>ShareList</title><meta charset="utf8"><meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"><meta name="referrer" content="never">
  <script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
  </head>
  <style>
    body{
      font-size:14px;
      line-height:1.7em;
    }
    .guide{
      width: 80%;
      margin: 10% auto;
      max-width: 720px;
      word-wrap:break-word;
      word-break:normal; 
    }
    .l-center{
      margin:auto;
      text-align:center;
    }
    .label{
      font-size:12px;
      color:rgba(0,0,0,.5);
    }
    h3{
      font-size:16px;
      text-align:center;
    }
    .auth p,.auth a{
      font-size:12px;
    }
    .auth a{
      color:#337ab7;
    }
    
    .form-item{
      margin-bottom:8px;
    }
    .form-item.show{
      display:block;
    }
    input[type='text'],select,button{
      width:100%;padding:8px;
      box-sizing:border-box;
    }
    button{
      background-color: #0078e7;
      color: #fff;
      border:none;
      outline:none;
      padding:8px;
      border-radius:5px;
    }
    .tab{
      display:none;
    }
  </style>
  <body>${cnt}</body></html>`
}

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('<head>', `<head><script>window.MANAGE_BASE="${basePath}"</script>`).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<file>}
   */
  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<string> 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<string>} 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<string>} 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 () => (
      <ConfigProvider locale={zhCN}>
        <RouterView />
      </ConfigProvider>
    )
  },
})

================================================
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,d09GMgABAAAAADdcABEAAAAAgrwAADb5AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmobgVIchHoGYACODggiCYJzEQgKgc9EgbNKC4QyAAE2AiQDhDIEIAWFGgeKMAxdG11xFWybtbHbAZw/+baXzUbYsHGw8WKcjqJ0k66l+f9zAhUZW3tMt4OgKhRFetqls6nUyZauNEK46K6B90zLVBKdTIfFxoblG9uFaXkYFge3sWDZMTFZGz/S6AcLuqXzvW1bTsvyoz99IhiWR0GBwy73x155uSM09kmu//x72bkvKfm1SAUvRE6nlAdobm1shJ70Km7B4lYNq2ZjY0mNlFasBAMjH6zAiH7FjPfNwH4rPqyXClu06tlXgsdIHAahwRCEFcSTIAwGe88Dtl/55h+QY7aMjlghqMuqE651m1oSpvGoKzRhPML4fDr1n04B4HMIhCE0BcmaWlNjmYMwl6epT91UwKkrDSPgRHz8t2lvIDmZYVMxIapQlyfA1jb0jpNfM/q7/FY1qovHhRD++f5i536xoJklSwKKdjxO8h0PAsyzQGN6iQujezW16olJIVmWZFsOaXcvJ3jFn0aAH0PvIXwI/v/pJqwMY3fztX272rbC2s6IaOOQOGI58P/vNLpt3Sf34KkVPEQ+3TSJ4giLC/epehV+CFgFOOMRbOrm7iagAEAEzL9Os+X/pShRDu0CTYc4bLQVeBkIpq//v2N9fess04EVoAPbB3ZScJyS7IJL7HdTelMRN0PBUHAJebrxtnZrh2ljGMapy14K5paP+WAxJUMogmtqsw36+hM1NzsPchiwkC0eYjT5ILVOBYu1wvOlarZ6+w8Kc1UKlQZzTU8OVVRO07upgOU3McvlaiWZjlQYuwuxChlJBgk4htq5qQr7Y6/VPszIFDPNTQghiF6p33fHmNofGaumI59SQgyvJyWAMtX8Gv467rXv2Sv99+x2b1IICTLGCK78pwUB5AEAwChh7MA5UIFzpgXnwQAuhBfcVEFw04XFmSkKbo4ScAuVgVuhBtw6TeB2GAJut2HiPPQQjwCy5lmIn36bWgJhf38jDM7xhj8E5YV7KworJAAj/w3As9smEttmJM/KkSL9TyCcAqQFNscWQD0eGAg4IXJtjp1I37Qsif/ff9chnHIVKkvJgfN3TRSKdNruOm9Evqiv84u3DGnyPKJXhgwFIKpkyFIEot9nIUeJ8r43EkflcVfco7EI6ogFjqEJdsVjO7KB4W42ofYn+g/lScBdIVKUmfWyxh/VmSEJ0x6wy9LszZsO4SEFRtwNDUxGgaJVPn9mnBTxEiRKkixFqjTpMmTLkSlLFGp/mf/5TySUWJhwESJFiRUnWgyG2Wci4Imn5DtqqCRX8KZXJLlDOUSVj+IwOJA1xcvQ2qSQL4z1cZsW0wbedeafOEC5Soo4jDoSOH0hQG2hIEyCBKOoFCHHqdelNZQe0Gp0w4D1Iyic12EJoItZVMNCfihpEImrdhLArstOMIQsHwhtkQYQjBUkpFCechwPwAAMnIcn0OtqnYUR+P8/9+7BYeCelP4F+B0A6M1mAwQIAF8N6u6XB1YLoYyJIMuxI8OvD5Y/o3iSLGgYFSk3XIvDUpRoMUIkBde5U7uze08okABSQDrIAmVgNmhlv7CWO5024z7rOYOO/v///wECsWwmxSqMsIxYqlKtSQrsANLwTBAHknaMFNSExoGNPKrgjsL6DjyXHWDDl6v7arxqfqbAAJQeXT27jneVdi3psnXJH7Y9XHtY8+DWg4txOHD+YFgChnVg2BYj8AdLB2yWcF6r1cOyEjnrkq+KVNmrzVf/eLxT56S7TnjllNOaNWlx3hfPvPFcm+UGrfHaS+2hTFOmWKESb70Pho9y3dZhsU/B0emD3X4Y8q8tHobAN3nuqFaj1gVT2Tk4ubjlem2cPD5++QJC+hisVp16DRr18sZ6ffXT3wADDTGIV5MZrrjuqmtukAeA/wgGAFgFBoHERQVHMzW69hh2xnS/LM9iGRFbGw6uFUE2xfMyvokJLEpoQiLriGmMlC3HpLTepfM2vfcZTM9oB5MicRVbVYkNxayu1MfKLK/cDxWGIzXCmkZaX7O1tfg0li1EgIqRJzjXE2U9VdwzhT1X0gtve+n9eLVAFJngY2/k9tbt3unovXt98Gl8XKDAmaCzz1p98dX3vhnqu55++NBPD7dfO2Uc+O1bf+T11x3/iFQX6gKxFPalmipNNv9yNJT7jZOiyISvGEyrxEalWsyo1Q5tuq2sR2O91uszZFPDdjZCBdsHUIeMFQNoWcb4AbQiY0M9ewgejYd+MPaYoPnZM5ApGcBejIUREIpAsYz79UI5qJBxuZ9Qqags11WrcVWtWjc0avRIq1ZndOrSZcCgm6ZNK5eS8vO9CyeUglbjBZ+lpfU+N+C21K9dCK5FRv0A7RIZFQOGpxuVQbF2Qy75KNMjvCafDAKewOvgyTNzD0xnTDKL7zeDX/0w+zUe33OYDG77s81CtmplzKHbDXK+zQTXaoI0W2QR2sqcBq1zLrhqNHaxGOO+Sy8nC7h9Bl+3UY5y+C9qIjOwuOvlgEu7xRjWyl4k+DWtt80EjyTyKI4owIIYYlCZ2N+2uQmtk1QlrZ9zzbuMM3uZMWzXyiwW5jAe+vfAw3kWTYu5E1VVRNOxXqw0TPIkNk+uZBx1MavDUye010W2tcZinOUFZRdrtGJ5Jh4uNpdAGvaZwOxO6xwmOfOfVc+glc3QCrGhlb2z2dtMpLROLrocdo1OS9oIZDDQ+wrrgcr9rD1l3kqVK8A008BfgnEpMP1rJADoSC/Vo8RSlTiPjPuOytNSmhIvPZIdbzHTIUIQj4a7S1Xp6MI4Oo7nVJeWOu5E4hzoVNWpN4JF9RRMUi2tuJC5eKwGYhGxMmkc77jEh2QWduJbZegcncYKiw5J4fB1vuMhG59NE+nItSyUWnMWqw/WFo4PBdSg41ZRPZddPFFRvfZNWjd+qernMWucUuuI0YS9KvK7vvRT12sRO62Tcq4+8tzye895p0088rfbsg1VZrPjxXsbrZueG33c+LBz3m9sbq3lGQ7xPMuEoLSgR2Ep376l29bwOjrLKIb52fZKFi9UT93iFKxV7TYMCxMm62zrvc06XdhV8fQ0PBUjpZZaa6mUluqOF5ZpXR8yvH19js6qi9EwR/90OQClRM6VFKQUilzSrnOSXXQ/vgfPfbqLfgnKpBDODjJwhq3gCtF14aIQA1HsjGKuYVLYQEyJxxw70bRAtyKgEbZSOvJOxgDJYPprL2R1asyBg5M5yoa1omFY3bRKIE5AIFsDYjabZqcMtDtsBCiDoDEcxAyTnrrdKlcFrxtotiAsCPekAHEv+jjbejO0Y9grSOl1GI/yz9YdyD1+q8QXWfjZMO8jhW+B+Wf+gOyrdNhiMxlari5Oz6FnSrpmTeawVXPb7CuXn9Isdi6GSWVfjVqx4mcpbxGfpXrwnuycpu5w3Y7jifytpshhWecmBJA4X1NjzYmmJvI0w+FD4ss0zjMD9NB2hDUMx7C2dFU5bEGMTykkybbURAk3eTuxgF7ZIqjTHY7xIvncZBu9SvfZoUjcYYHc6HuZ247PM1s9pPvOrJnZf2O0x4vypXD8OpTbyjUYuxVzqJ
Download .txt
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
Download .txt
SYMBOL INDEX (285 symbols across 79 files)

FILE: packages/sharelist-core/lib/action.js
  method ls (line 41) | async ls(path) {
  method stat (line 49) | async stat(path) {
  method get (line 57) | async get(path, options) {
  method upload (line 82) | async upload(path, stream, { size }) {
  method mkdir (line 110) | async mkdir(path) {
  method rm (line 116) | async rm(path) {
  method mv (line 122) | async mv(path, destPath, copy) {

FILE: packages/sharelist-core/lib/index.js
  constant DRIVE_KEY (line 17) | const DRIVE_KEY = Symbol('drive_key')
  method get (line 117) | get() {
  method set (line 120) | set(data) {

FILE: packages/sharelist-core/lib/rectifier.js
  method write (line 108) | write(chunk, encoding, callback) {
  method final (line 155) | final(callback) {

FILE: packages/sharelist-core/lib/request.js
  class LRUCache (line 11) | class LRUCache {
    method constructor (line 12) | constructor(size = 10) {
    method update (line 17) | update(key) {
    method get (line 26) | get(key) {
    method set (line 34) | set(key, val) {

FILE: packages/sharelist-core/lib/utils.js
  constant YAML (line 7) | const YAML = require('yaml')
  function zeroize (line 53) | function zeroize(v) {
  method constructor (line 263) | constructor(size = 10) {
  method update (line 268) | update(key) {
  method get (line 277) | get(key) {
  method set (line 285) | set(key, val) {
  method transform (line 385) | transform(chunk, encoding, callback) {

FILE: packages/sharelist-manage/src/App.tsx
  method setup (line 7) | setup() {

FILE: packages/sharelist-manage/src/components.d.ts
  type GlobalComponents (line 9) | interface GlobalComponents {

FILE: packages/sharelist-manage/src/components/code-editor/index.tsx
  method setup (line 13) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/components/icon/icon-svg.js
  function n (line 21) | function n() {

FILE: packages/sharelist-manage/src/components/player/index.tsx
  method setup (line 45) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/components/sider/index.tsx
  method setup (line 11) | setup() {

FILE: packages/sharelist-manage/src/hooks/useApi.ts
  type APIItem (line 7) | type APIItem = [
  type APICall (line 14) | type APICall = (...rest: Array<any>) => Promise<AxiosResponse<ReqResponse>>
  type IUseApi (line 16) | interface IUseApi {
  type RequestMethod (line 22) | type RequestMethod = 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELE...
  type ReqConfig (line 24) | type ReqConfig = {
  type ReqResponse (line 59) | type ReqResponse = {
  type APIOptions (line 65) | interface APIOptions {
  type a (line 120) | type a = typeof apis
  method install (line 135) | install(app: App) {
  function createRequest (line 149) | function createRequest(api: APIItem, defaultOptions?: APIOptions): APICa...

FILE: packages/sharelist-manage/src/hooks/useClipboard.ts
  type Type (line 3) | type Type = 'file' | 'string'

FILE: packages/sharelist-manage/src/hooks/useConfirm.ts
  method onOk (line 11) | onOk() {
  type ConfirmOption (line 18) | type ConfirmOption = {
  method onOk (line 33) | onOk() {

FILE: packages/sharelist-manage/src/hooks/useHooks.ts
  function safeOnMounted (line 3) | function safeOnMounted(hook: () => any): void {
  type ToggleValue (line 12) | type ToggleValue = number | string | boolean | undefined
  type useTitleOptions (line 120) | type useTitleOptions = {
  method true (line 146) | true() {
  method false (line 155) | false() {

FILE: packages/sharelist-manage/src/hooks/useLocalStorage.ts
  type LocalStateKey (line 3) | type LocalStateKey = string
  function useLocalStorageState (line 5) | function useLocalStorageState<T = any>(key: LocalStateKey, defaultValue?...

FILE: packages/sharelist-manage/src/hooks/useRequest.ts
  type Service (line 46) | type Service<D, P extends any[]> = (...args: P) => Promise<D>
  type RequestOptions (line 48) | interface RequestOptions<T, P> {
  type RequestState (line 65) | type RequestState<D, P extends Array<any>> = {
  type RequestActions (line 72) | interface RequestActions<D, P extends Array<any>> {
  type RequestCore (line 81) | interface RequestCore<D, P extends any[]> extends RequestActions<D, P> {
  type RequestResult (line 86) | type RequestResult<D, P extends any[]> = RequestActions<D, P> &
  type PluginResult (line 91) | interface PluginResult<D = any, P extends any[] = any> {
  type Plugin (line 108) | interface Plugin<D, P extends Array<any>> {
  type PickMethod (line 112) | type PickMethod<M, C> = {
  method before (line 285) | before() {
  method finally (line 288) | finally() {
  method cancel (line 295) | cancel() {
  method before (line 313) | before() {
  method finally (line 324) | finally() {
  method cancel (line 327) | cancel() {
  method before (line 350) | before() {

FILE: packages/sharelist-manage/src/hooks/useScroll.ts
  type RequestOptions (line 3) | interface RequestOptions<T, P> {
  type actions (line 12) | interface actions<T> {
  type useScrollOption (line 20) | type useScrollOption = {

FILE: packages/sharelist-manage/src/hooks/useSetting.ts
  type IUseSetting (line 8) | type IUseSetting = {
  type IUseSettingResult (line 12) | interface IUseSettingResult {
  type ConfigFieldItem (line 32) | type ConfigFieldItem = {
  type fieldGroup (line 41) | type fieldGroup = {

FILE: packages/sharelist-manage/src/hooks/useStore.ts
  type InjectType (line 3) | type InjectType = 'root' | 'optional'
  type FunctionalStore (line 5) | interface FunctionalStore<T> {
  type BridgeValue (line 52) | type BridgeValue = {
  type BridgeFunctions (line 56) | type BridgeFunctions = 'getGrayScaleValue'
  type GrayParams (line 71) | type GrayParams = 'code1' | 'code2'
  type GrapValue (line 73) | type GrapValue = {
  type ICdpSpaceInfo (line 78) | type ICdpSpaceInfo = {
  function getCdpSpaceInfo (line 81) | function getCdpSpaceInfo<T>(spaceCode: string): Promise<ICdpSpaceInfo> {

FILE: packages/sharelist-manage/src/hooks/useUrlState.ts
  type initial (line 4) | type initial = {

FILE: packages/sharelist-manage/src/hooks/useWorker.ts
  type Fn (line 3) | type Fn = (...args: any[]) => any
  function invoke (line 8) | function invoke(name: string, params: unknown[], id: string) {
  class WorkerFactory (line 61) | class WorkerFactory {
    method constructor (line 64) | constructor() {

FILE: packages/sharelist-manage/src/hooks/utils.ts
  function onUnmounted (line 14) | function onUnmounted(cb: () => any): void {

FILE: packages/sharelist-manage/src/main.ts
  method onReq (line 37) | onReq(params, options) {

FILE: packages/sharelist-manage/src/store/index.ts
  method saveToken (line 12) | saveToken(token: string) {
  method removeToken (line 15) | removeToken() {
  method setLayout (line 18) | setLayout(val: string) {
  method savePath (line 21) | savePath(input: string) {

FILE: packages/sharelist-manage/src/types/IDrive.ts
  type DrivePath (line 1) | type DrivePath = {
  type DriverField (line 6) | type DriverField = {
  type IDrive (line 17) | type IDrive = {
  type IPlugin (line 22) | type IPlugin = {
  type DriverGuide (line 28) | type DriverGuide = {
  type Driver (line 34) | type Driver = {
  type IFile (line 40) | type IFile = {

FILE: packages/sharelist-manage/src/types/shim.d.ts
  type ISetting (line 7) | type ISetting = {

FILE: packages/sharelist-manage/src/utils/format.ts
  constant EXT_IMAGE (line 1) | const EXT_IMAGE = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 't...
  constant EXT_AUDIO_SUPPORT (line 3) | const EXT_AUDIO_SUPPORT = ['mp3', 'm4a', 'acc', 'wav', 'ogg', 'flac']
  constant EXT_VIDEO_SUPPORT (line 5) | const EXT_VIDEO_SUPPORT = ['mp4', 'mpeg', '3gp', 'mkv']

FILE: packages/sharelist-manage/src/views/disk/index.tsx
  method setup (line 27) | setup() {

FILE: packages/sharelist-manage/src/views/disk/partial/action/index.tsx
  type IFileError (line 17) | interface IFileError extends IFile {
  method setup (line 26) | setup(props, { emit }) {
  method setup (line 50) | setup(props, { emit }) {
  method onOk (line 420) | onOk() {
  method onCancel (line 423) | onCancel() {
  method onOk (line 527) | onOk() {
  method onCancel (line 530) | onCancel() {

FILE: packages/sharelist-manage/src/views/disk/partial/auth/index.tsx
  method setup (line 17) | setup(props) {

FILE: packages/sharelist-manage/src/views/disk/partial/breadcrumb/index.tsx
  method setup (line 18) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/views/disk/partial/error/index.tsx
  method setup (line 14) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/views/disk/partial/meta/index.tsx
  method setup (line 19) | setup(props) {
  method setup (line 60) | setup(props) {
  method setup (line 106) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/views/disk/partial/modifier/index.tsx
  type FormState (line 10) | type FormState = {
  method setup (line 94) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/views/disk/partial/search/index.tsx
  method setup (line 8) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/views/disk/partial/task/index.tsx
  type STATUS (line 12) | enum STATUS {
  type ITask (line 22) | type ITask = {
  type TaskSet (line 41) | type TaskSet = {
  method setup (line 47) | setup(props, ctx) {

FILE: packages/sharelist-manage/src/views/disk/partial/upload/index.tsx
  type readChunkedOptions (line 22) | interface readChunkedOptions {
  function readChunked (line 27) | function readChunked(file: File, { onChunk, onFinish, chunkSize }: readC...
  method onChunk (line 90) | onChunk(chunk: any, offset: number) {
  method onFinish (line 120) | onFinish(err: Error) {
  type IUseUpload (line 138) | type IUseUpload = {
  type IUseUploadResult (line 143) | interface IUseUploadResult {
  method setup (line 381) | setup(props, { slots }) {

FILE: packages/sharelist-manage/src/views/disk/partial/useDisk.ts
  type Handler (line 8) | interface Handler {
  type IUseDiskOption (line 43) | type IUseDiskOption = {
  type IUseDisk (line 50) | type IUseDisk = {
  type IQuery (line 54) | type IQuery = {
  type DiskState (line 63) | type DiskState = {
  type IUseDiskAction (line 71) | type IUseDiskAction = any

FILE: packages/sharelist-manage/src/views/general/index.tsx
  method setup (line 34) | setup(props, { slots }) {
  method setup (line 39) | setup() {

FILE: packages/sharelist-manage/src/views/home/index.tsx
  method setup (line 12) | setup() {

FILE: packages/sharelist-manage/src/views/plugin/index.tsx
  method setup (line 22) | setup() {

FILE: packages/sharelist-manage/src/views/plugin/partial/store/index.tsx
  method setup (line 13) | setup() {

FILE: packages/sharelist-manage/src/views/signin/index.tsx
  method setup (line 7) | setup() {

FILE: packages/sharelist-web/src/App.tsx
  method setup (line 6) | setup() {

FILE: packages/sharelist-web/src/components/icon/icon-svg.js
  function n (line 21) | function n() {

FILE: packages/sharelist-web/src/components/player/index.tsx
  method setup (line 45) | setup(props, ctx) {

FILE: packages/sharelist-web/src/config/api.ts
  type IAPI (line 1) | type IAPI = [

FILE: packages/sharelist-web/src/hooks/useApi.ts
  type APIItem (line 7) | type APIItem = [
  type APICall (line 14) | type APICall = (...rest: Array<any>) => Promise<AxiosResponse<ReqResponse>>
  type IUseApi (line 16) | interface IUseApi {
  type RequestMethod (line 22) | type RequestMethod = 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELE...
  type ReqConfig (line 24) | type ReqConfig = {
  type ReqResponse (line 59) | type ReqResponse = {
  type APIOptions (line 65) | interface APIOptions {
  type a (line 120) | type a = typeof apis
  method install (line 135) | install(app: App) {
  function createRequest (line 149) | function createRequest(api: APIItem, defaultOptions?: APIOptions): APICa...

FILE: packages/sharelist-web/src/hooks/useDisk.ts
  type IFile (line 8) | type IFile = {
  type IUseDiskOption (line 63) | type IUseDiskOption = {
  type IUseDisk (line 70) | type IUseDisk = {
  type IQuery (line 74) | type IQuery = {
  type DiskState (line 83) | type DiskState = {
  type IUseDiskAction (line 91) | type IUseDiskAction = any

FILE: packages/sharelist-web/src/hooks/useHooks.ts
  function safeOnMounted (line 3) | function safeOnMounted(hook: () => any): void {
  type ToggleValue (line 13) | type ToggleValue = number | string | boolean | undefined
  type useTitleOptions (line 122) | type useTitleOptions = {
  type delayToggleActions (line 145) | interface delayToggleActions {
  method true (line 152) | true(immediate = false) {
  method false (line 161) | false() {

FILE: packages/sharelist-web/src/hooks/useLocalStorage.ts
  type LocalStateKey (line 3) | type LocalStateKey = string
  function useLocalStorageState (line 5) | function useLocalStorageState<T = any>(key: LocalStateKey, defaultValue?...

FILE: packages/sharelist-web/src/hooks/useScroll.ts
  type RequestOptions (line 3) | interface RequestOptions<T, P> {
  type actions (line 12) | interface actions<T> {
  type useScrollOption (line 19) | type useScrollOption = {

FILE: packages/sharelist-web/src/hooks/useSetting.ts
  type IUseConfig (line 4) | type IUseConfig = {

FILE: packages/sharelist-web/src/hooks/utils.ts
  function onUnmounted (line 14) | function onUnmounted(cb: () => any): void {

FILE: packages/sharelist-web/src/store/index.ts
  method setLayout (line 11) | setLayout(val: string) {

FILE: packages/sharelist-web/src/types/IDrive.ts
  type DrivePath (line 1) | type DrivePath = {
  type DriverField (line 6) | type DriverField = {
  type IDrive (line 17) | type IDrive = {
  type IPlugin (line 22) | type IPlugin = {
  type DriverGuide (line 28) | type DriverGuide = {
  type Driver (line 34) | type Driver = {

FILE: packages/sharelist-web/src/types/shim.d.ts
  type ISetting (line 12) | type ISetting = {

FILE: packages/sharelist-web/src/utils/format.ts
  constant EXT_IMAGE (line 1) | const EXT_IMAGE = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 't...
  constant EXT_AUDIO_SUPPORT (line 3) | const EXT_AUDIO_SUPPORT = ['mp3', 'm4a', 'acc', 'wav', 'ogg', 'flac']
  constant EXT_VIDEO_SUPPORT (line 5) | const EXT_VIDEO_SUPPORT = ['mp4', 'mpeg', '3gp', 'mkv']

FILE: packages/sharelist-web/src/views/home/index.tsx
  method setup (line 19) | setup() {

FILE: packages/sharelist-web/src/views/home/partial/auth/index.tsx
  method setup (line 17) | setup(props) {

FILE: packages/sharelist-web/src/views/home/partial/breadcrumb/index.tsx
  method setup (line 18) | setup(props, ctx) {

FILE: packages/sharelist-web/src/views/home/partial/error/index.tsx
  method setup (line 14) | setup(props, ctx) {

FILE: packages/sharelist-web/src/views/home/partial/header/index.tsx
  method setup (line 13) | setup() {

FILE: packages/sharelist-webdav/src/context.ts
  method get (line 19) | get(field: string): string | undefined {

FILE: packages/sharelist-webdav/src/index.ts
  type WebDAVAuth (line 9) | interface WebDAVAuth {
  type WebDAVServerOptions (line 12) | type WebDAVServerOptions = {
  class WebDAVServer (line 19) | class WebDAVServer {
    method constructor (line 34) | constructor({ driver, base, redirect, auth }: WebDAVServerOptions = { ...
    method createContext (line 49) | createContext(req: http.IncomingMessage, options: WebDAVServerOptions)...
    method request (line 85) | async request(req: WebDAVRequest, options: WebDAVServerOptions): Promi...

FILE: packages/sharelist-webdav/src/operations/shared.ts
  constant DEFAULT_PROPS (line 28) | const DEFAULT_PROPS = [

FILE: packages/sharelist-webdav/src/types.ts
  type DriverMethod (line 3) | type DriverMethod = 'stat' | 'get' | 'ls' | 'rm' | 'mkdir' | 'upload' | ...
  type WebDAVDepth (line 5) | type WebDAVDepth = "0" | "1" | "1,noroot" | "infinity"
  type WebDAVRequest (line 7) | type WebDAVRequest = http.IncomingMessage
  type DriverMethodResponse (line 9) | type DriverMethodResponse = {
  type Driver (line 15) | type Driver = {
  type WebDAVAuthRecord (line 19) | type WebDAVAuthRecord = {
  type Context (line 23) | interface Context {
  type WebDAVMethod (line 37) | interface WebDAVMethod {
  type Response (line 41) | type Response = {

FILE: packages/sharelist/app/modules/command/index.js
  method ls (line 41) | async ls(path) {
  method stat (line 59) | async stat(path) {
  method get (line 67) | async get(path, options) {
  method upload (line 92) | async upload(path, stream, { size }) {
  method mkdir (line 122) | async mkdir(path) {
  method rm (line 128) | async rm(path) {
  method mv (line 134) | async mv(path, destPath, copy) {

FILE: packages/sharelist/app/modules/core/http.js
  class http (line 1) | class http {
    method constructor (line 8) | constructor(app, config) {
    method pwd (line 13) | pwd(id) {
    method get (line 18) | async get(id) {
    method list (line 34) | async list(id) {
    method getFileSize (line 38) | async getFileSize(url, headers = {}) {
  class https (line 64) | class https extends http {
    method constructor (line 70) | constructor(app, config) {

FILE: packages/sharelist/app/modules/core/index.js
  method onUpdate (line 27) | onUpdate(id) {
  method onRemove (line 31) | onRemove(hash) {

FILE: packages/sharelist/app/modules/core/plugin.js
  class Plugin (line 64) | class Plugin {
    method constructor (line 65) | constructor(options) {
    method scanMeta (line 74) | scanMeta() {
    method load (line 106) | load() {
    method get (line 125) | get(id) {
    method set (line 137) | set(id, data) {
    method createFromUrl (line 154) | async createFromUrl(url) {
    method getFromStore (line 162) | async getFromStore() {
    method remove (line 192) | remove(id) {
    method replaceSource (line 203) | replaceSource(url) {
    method upgrade (line 208) | async upgrade(id) {
    method getSources (line 229) | getSources() {

FILE: packages/sharelist/app/modules/core/task.js
  constant STATUS (line 31) | const STATUS = {

FILE: packages/sharelist/app/modules/guide/index.js
  method config (line 10) | config() {

FILE: packages/sharelist/app/modules/server/controller.js
  method page (line 127) | async page(ctx, next) {
  method setting (line 180) | async setting(ctx, next) {
  method userConfig (line 183) | async userConfig(ctx, next) {
  method configField (line 187) | async configField(ctx, next) {
  method reload (line 198) | async reload(ctx, next) {
  method reloadBench (line 202) | async reloadBench(ctx) {
  method updateSetting (line 208) | async updateSetting(ctx, next) {
  method getPlugin (line 223) | async getPlugin(ctx, next) {
  method setPlugin (line 233) | async setPlugin(ctx, next) {
  method removePlugin (line 242) | async removePlugin(ctx) {
  method upgradePlugin (line 251) | async upgradePlugin(ctx) {
  method clearCache (line 260) | async clearCache(ctx, next) {
  method getPath (line 265) | async getPath(ctx, next) {
  method get (line 278) | async get(ctx, next) {
  method list (line 297) | async list(ctx, next) {
  method cancelUpload (line 303) | async cancelUpload(ctx) {
  method createUpload (line 309) | async createUpload(ctx) {
  method upload (line 344) | async upload(ctx) {
  method pluginStore (line 380) | async pluginStore(ctx) {
  method installPlugin (line 384) | async installPlugin(ctx) {
  method tasks (line 394) | async tasks(ctx) {
  method transfer (line 399) | async transfer(ctx) {
  method removeTransfer (line 403) | async removeTransfer(ctx) {
  method resumeTransfer (line 407) | async resumeTransfer(ctx) {
  method pauseTransfer (line 411) | async pauseTransfer(ctx) {
  method retryTransfer (line 415) | async retryTransfer(ctx) {
  method remove (line 420) | async remove(ctx) {
  method mkdir (line 427) | async mkdir(ctx) {
  method hashSave (line 441) | async hashSave(ctx) {
  method update (line 451) | async update(ctx) {
  method removeDisk (line 472) | async removeDisk(ctx) {

FILE: packages/sharelist/app/modules/server/index.js
  class Server (line 9) | class Server {
    method constructor (line 10) | constructor(sharelist, appConfig) {
    method use (line 36) | use(module) {
    method createConfig (line 40) | createConfig() {
    method start (line 53) | start() {
  function createServer (line 58) | function createServer(...args) {

FILE: packages/sharelist/app/modules/server/runtime.js
  method read (line 343) | read(size) {

FILE: scripts/changelog.js
  constant COMMIT_PATTERN (line 3) | const COMMIT_PATTERN = /^([^)]*)(?:\(([^)]*?)\)|):(.*?(?:\[([^\]]+?)\]|)...
  constant SEPARATOR (line 7) | const SEPARATOR = '===END==='
  constant TYPES (line 9) | const TYPES = {

FILE: scripts/release.js
  function main (line 56) | async function main() {
  function updateVersion (line 165) | function updateVersion(version) {
  function publishPackage (line 175) | async function publishPackage(version, run) {
Condensed preview — 226 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (643K chars).
[
  {
    "path": ".eslintrc.js",
    "chars": 1058,
    "preview": "module.exports = {\n  parser: 'vue-eslint-parser',\n  parserOptions: {\n    // set script parser\n    parser: '@typescript-e"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 961,
    "preview": "name: \"Bug report\"\ndescription: 问题报告\nlabels: [pending triage]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n  "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 218,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions & Discussions\n    url: https://github.com/reruin/sharelis"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 623,
    "preview": "name: \"Feature request\"\ndescription: 功能需求\nlabels: [\"enhancement: pending triage\"]\nbody:\n  - type: textarea\n    id: featu"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2344,
    "preview": "name: CI\n\non:\n  push:\n    tags:\n      - 'v*'\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1736,
    "preview": "name: Docker\n\non:\n  push:\n    # branches:\n    #   - next\n    tags:\n      - 'v*'\njobs:\n  build:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".gitignore",
    "chars": 254,
    "preview": "node_modules\nbuild/\ndist/\n/packages/sharelist/manage/\n/packages/sharelist/theme/\n/packages/sharelist/cache/\n/packages/sh"
  },
  {
    "path": ".prettierrc.js",
    "chars": 320,
    "preview": "// https://prettier.io/docs/en/configuration.html\nmodule.exports = {\n  //分号终止符\n  semi: false,\n\n  //行尾逗号\n  trailingComma:"
  },
  {
    "path": ".yarnrc",
    "chars": 129,
    "preview": "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile v1\n\n\nyarn-path \".yarn/releases/yarn-1.1"
  },
  {
    "path": "LICENSE",
    "chars": 1098,
    "preview": "MIT License\n\nCopyright (c) 2018-present, Reruin and Sharelist contributors\n\nPermission is hereby granted, free of charge"
  },
  {
    "path": "README.md",
    "chars": 1316,
    "preview": "# ShareList\n\n[![Build Status](https://github.com/reruin/sharelist/actions/workflows/ci.yml/badge.svg)](https://github.co"
  },
  {
    "path": "docs/.nojekyll",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/README.md",
    "chars": 498,
    "preview": "# ShareList\n\n[![Build Status](https://github.com/reruin/sharelist/actions/workflows/ci.yml/badge.svg)](https://github.co"
  },
  {
    "path": "docs/_navbar.md",
    "chars": 60,
    "preview": "- Translations\n  - [:uk: English](/)\n  - [:cn: 中文](/zh-cn/)\n"
  },
  {
    "path": "docs/en/README.md",
    "chars": 469,
    "preview": "# ShareList\n\n[![Build Status](https://api.travis-ci.com/reruin/sharelist.svg?branch=master)](https://travis-ci.com/rerui"
  },
  {
    "path": "docs/en/_navbar.md",
    "chars": 52,
    "preview": "- Translations\n  - [中文](/zh-cn/)\n  - [English](/en/)"
  },
  {
    "path": "docs/en/_sidebar.md",
    "chars": 102,
    "preview": "* [Installation](en/installation.md)\n* [Configuration](en/configuration.md)\n* [Advance](en/advance.md)"
  },
  {
    "path": "docs/en/installation.md",
    "chars": 36,
    "preview": "# Installation\n\nTranslation needed!\n"
  },
  {
    "path": "docs/index.html",
    "chars": 1371,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <title>ShareList Docs</title>\n  <meta http-equiv=\"X-"
  },
  {
    "path": "docs/zh-cn/README.md",
    "chars": 332,
    "preview": "# ShareList\n\n[![Build Status](https://github.com/reruin/sharelist/actions/workflows/ci.yml/badge.svg)](https://github.co"
  },
  {
    "path": "docs/zh-cn/_navbar.md",
    "chars": 52,
    "preview": "- Translations\n  - [中文](/zh-cn/)\n  - [English](/en/)"
  },
  {
    "path": "docs/zh-cn/_sidebar.md",
    "chars": 81,
    "preview": "* [安装](zh-cn/installation.md)\n* [配置](zh-cn/configuration.md)\n* [开发](zh-cn/dev.md)"
  },
  {
    "path": "docs/zh-cn/advance.md",
    "chars": 870,
    "preview": "## 目录加密\n在需加密目录内新建 ```.passwd``` 文件(此项可修改),```type```为验证方式,```data```为验证内容。  \n例如:    \n```yaml\ntype: basic\ndata:\n  - 12345"
  },
  {
    "path": "docs/zh-cn/configuration.md",
    "chars": 1353,
    "preview": "\n## 常规\n访问 ```http://localhost:33001/@manage```,填写口令即可进入后台管理。\n\n### 后台管理\n设置后台管理密码。默认 ```sharelist```。\n\n### 网站标题\n设置网站标题。\n\n#"
  },
  {
    "path": "docs/zh-cn/dev.md",
    "chars": 2072,
    "preview": "## 接口\n\n### 获取文件列表\n```\nPOST /api/drive/list\n```\n请求参数\n参数名 | 含义 | 是否必须\n-|-|-\npath|路径|是\n\n返回结果\n名称 | 类型 | 说明\n-|-|-\nid|string|目"
  },
  {
    "path": "docs/zh-cn/installation.md",
    "chars": 454,
    "preview": "# 安装\nSharelist支持多种安装方式。\n\n## Docker\n```bash\ndocker run -d -v /etc/sharelist:/sharelist/cache -p 33001:33001 --name=\"share"
  },
  {
    "path": "package.json",
    "chars": 1273,
    "preview": "{\n  \"name\": \"sharelist-monorepo\",\n  \"private\": true,\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"engines\": {\n    \"node\": "
  },
  {
    "path": "packages/sharelist/CHANGELOG.md",
    "chars": 4045,
    "preview": "## [0.4.4](https://github.com/reruin/sharelist/compare/v0.4.3...v0.4.4) (2025-06-13)\n\n\n\n## [0.4.3](https://github.com/re"
  },
  {
    "path": "packages/sharelist/Dockerfile",
    "chars": 204,
    "preview": "FROM node:20-alpine\nLABEL maintainer=reruin\n\nADD . /sharelist/\nWORKDIR /sharelist\nVOLUME /sharelist/cache\n\nRUN npm insta"
  },
  {
    "path": "packages/sharelist/LICENSE",
    "chars": 1098,
    "preview": "MIT License\n\nCopyright (c) 2018-present, Reruin and Sharelist contributors\n\nPermission is hereby granted, free of charge"
  },
  {
    "path": "packages/sharelist/README.md",
    "chars": 18,
    "preview": "# ShareList Server"
  },
  {
    "path": "packages/sharelist/app/config.js",
    "chars": 517,
    "preview": "\nconst path = require('path')\n\nconst env = {\n  baseDir: path.join(__dirname, '../'),\n  cacheDir: path.join(!process.pkg "
  },
  {
    "path": "packages/sharelist/app/main.js",
    "chars": 1201,
    "preview": "\nconst createSharelist = require('./modules/core')\nconst createWebDAV = require('./modules/webdav')\nconst createServer ="
  },
  {
    "path": "packages/sharelist/app/modules/command/index.js",
    "chars": 5678,
    "preview": "/**\n * 文件名寻址操作函数\n * @param {*} v \n * @returns \n */\nconst parsePath = v => v.replace(/(^\\/|\\/$)/g, '').split('/').map(dec"
  },
  {
    "path": "packages/sharelist/app/modules/core/cache.js",
    "chars": 827,
    "preview": "const createDB = require('./db')\n\nmodule.exports = (path) => {\n  let { data, save } = createDB(path)\n\n  const get = (id)"
  },
  {
    "path": "packages/sharelist/app/modules/core/config.js",
    "chars": 2610,
    "preview": "const createDB = require('./db')\n\nconst qs = require('querystringify')\n\nconst { URL } = require('url')\n\nconst { watch } "
  },
  {
    "path": "packages/sharelist/app/modules/core/db.js",
    "chars": 1848,
    "preview": "const path = require('path')\n\nconst fs = require('fs')\n\nconst writeFileAtomic = require('write-file-atomic')\n\nconst mkdi"
  },
  {
    "path": "packages/sharelist/app/modules/core/http.js",
    "chars": 1650,
    "preview": "class http {\n  static options = {\n    protocol: \"http\",\n    singleton: true,\n    mountable: false\n  }\n\n  constructor(app"
  },
  {
    "path": "packages/sharelist/app/modules/core/index.js",
    "chars": 2329,
    "preview": "const path = require('path')\n\nconst { nanoid } = require('nanoid')\n\nconst { request, createPluginLoader } = require('@sh"
  },
  {
    "path": "packages/sharelist/app/modules/core/plugin.js",
    "chars": 6116,
    "preview": "const { nanoid } = require('nanoid')\n\nconst crypto = require('crypto')\n\nconst fs = require('fs')\n\nconst path = require('"
  },
  {
    "path": "packages/sharelist/app/modules/core/task.js",
    "chars": 14526,
    "preview": "\n/**\n * type ITask = {\n *   total:number,\n *   success:number,\n *   error:number,\n *   totalSize:number,\n *   status: ST"
  },
  {
    "path": "packages/sharelist/app/modules/core/theme.js",
    "chars": 933,
    "preview": "const fs = require('fs')\n\nconst path = require('path')\n\nmodule.exports = (themeDir = []) => {\n  let themes = []\n\n  const"
  },
  {
    "path": "packages/sharelist/app/modules/core/upload.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/sharelist/app/modules/core/utils.js",
    "chars": 3547,
    "preview": "\nconst ignore = require('ignore')\nconst crypto = require('crypto')\nconst { pluginConfigKey } = require('./plugin')\n\n//过滤"
  },
  {
    "path": "packages/sharelist/app/modules/guide/driver/aliyundrive.js",
    "chars": 260,
    "preview": "const { getOAuthAccessToken, PROXY_URL, render } = require('./shared')\n\nmodule.exports = async function (ctx, next) {\n  "
  },
  {
    "path": "packages/sharelist/app/modules/guide/driver/baidu.js",
    "chars": 4015,
    "preview": "const { PROXY_URL, render, btoa, atob } = require('./shared')\nconst querystring = require('querystring')\n\nconst getOAuth"
  },
  {
    "path": "packages/sharelist/app/modules/guide/driver/googledrive.js",
    "chars": 5607,
    "preview": "const { PROXY_URL, render } = require('./shared')\n\nconst getOAuthAccessToken = async (app, url, { client_id, client_secr"
  },
  {
    "path": "packages/sharelist/app/modules/guide/driver/onedrive.js",
    "chars": 9828,
    "preview": "\nconst { PROXY_URL, render, btoa, atob } = require('./shared')\n\nconst querystring = require('querystring')\n\nconst suppor"
  },
  {
    "path": "packages/sharelist/app/modules/guide/driver/shared.js",
    "chars": 1492,
    "preview": "exports.PROXY_URL = 'https://reruin.github.io/sharelist/redirect.html'\n\nexports.render = (ctx, cnt) => {\n  return ctx.bo"
  },
  {
    "path": "packages/sharelist/app/modules/guide/index.js",
    "chars": 887,
    "preview": "const baidu = require('./driver/baidu')\nconst onedrive = require('./driver/onedrive')\nconst googledrive = require('./dri"
  },
  {
    "path": "packages/sharelist/app/modules/server/controller.js",
    "chars": 13243,
    "preview": "const fs = require('fs')\nconst { nanoid } = require('nanoid')\nconst path = require('path')\nconst { createRuntime, select"
  },
  {
    "path": "packages/sharelist/app/modules/server/index.js",
    "chars": 1469,
    "preview": "const Koa = require('koa')\nconst koaCors = require('@koa/cors')\nconst koaBody = require('koa-body')\nconst koaJson = requ"
  },
  {
    "path": "packages/sharelist/app/modules/server/router.js",
    "chars": 2966,
    "preview": "const Router = require('@koa/router')\nconst createAuth = (sharelist) => async (ctx, next) => {\n\n  let token = ctx.get('a"
  },
  {
    "path": "packages/sharelist/app/modules/server/runtime.js",
    "chars": 8198,
    "preview": "const { URLSearchParams } = require('url')\nconst promisify = require('util').promisify\nconst extname = require('path').e"
  },
  {
    "path": "packages/sharelist/app/modules/webdav/index.js",
    "chars": 2009,
    "preview": "const { WebDAVServer } = require('@sharelist/webdav')\n\nconst isWebDAVRequest = (ctx, webdavPath) => {\n  if (webdavPath ="
  },
  {
    "path": "packages/sharelist/app/shared/send.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/sharelist/app.js",
    "chars": 1186,
    "preview": "#!/usr/bin/env node\n\n/**\n * Module dependencies.\n */\n\nconst bootstrap = require('./app/main')\nconst http = require('http"
  },
  {
    "path": "packages/sharelist/docker-compose.yml",
    "chars": 150,
    "preview": "version: \"3\"\nservices:\n  sharelist:\n    image: reruin/sharelist\n    volumes:\n      - $HOME/sharelist:/sharelist/cache\n  "
  },
  {
    "path": "packages/sharelist/package.json",
    "chars": 1708,
    "preview": "{\n  \"name\": \"sharelist\",\n  \"version\": \"0.4.4\",\n  \"bin\": \"app.js\",\n  \"repository\": \"https://github.com/reruin/sharelist\","
  },
  {
    "path": "packages/sharelist-core/.prettierrc.js",
    "chars": 273,
    "preview": "// https://prettier.io/docs/en/configuration.html\nmodule.exports = {\n  //分号终止符\n  semi: false,\n\n  //行尾逗号\n  trailingComma:"
  },
  {
    "path": "packages/sharelist-core/CHANGELOG.md",
    "chars": 551,
    "preview": "## [0.1.7](https://github.com/reruin/sharelist/compare/v0.3.6...v0.1.7) (2021-10-14)\n\n\n### Bug Fixes\n\n* **core:** fix so"
  },
  {
    "path": "packages/sharelist-core/LICENSE",
    "chars": 1098,
    "preview": "MIT License\n\nCopyright (c) 2021-present, Reruin and Sharelist contributors\n\nPermission is hereby granted, free of charge"
  },
  {
    "path": "packages/sharelist-core/README.md",
    "chars": 1199,
    "preview": "# @sharelist/core [![npm](https://img.shields.io/npm/v/@sharelist/core.svg)](https://npmjs.com/package/@sharelist/core)\n"
  },
  {
    "path": "packages/sharelist-core/index.js",
    "chars": 40,
    "preview": "module.exports = require('./lib/index')\n"
  },
  {
    "path": "packages/sharelist-core/lib/action.js",
    "chars": 5201,
    "preview": "/**\n * 提供文件名寻址操作\n * ls / rm / mkdir / stat / upload / get / mv(rename)\n */\n\nconst parsePath = v => v.replace(/(^\\/|\\/$)/"
  },
  {
    "path": "packages/sharelist-core/lib/driver.js",
    "chars": 16743,
    "preview": "const utils = require('./utils')\nconst request = require('./request')\nconst { PassThrough } = require('stream')\n\nconst c"
  },
  {
    "path": "packages/sharelist-core/lib/index.js",
    "chars": 10583,
    "preview": "const { URL } = require('url')\n\nconst utils = require('./utils')\n\nconst createDriver = require('./driver')\n\nconst action"
  },
  {
    "path": "packages/sharelist-core/lib/rectifier.js",
    "chars": 6860,
    "preview": "const { resolve4 } = require('dns')\nconst { Readable, Writable, PassThrough } = require('stream')\nconst request = requir"
  },
  {
    "path": "packages/sharelist-core/lib/request.js",
    "chars": 7841,
    "preview": "const fetch = require('node-fetch')\n\nconst btoa = (v) => Buffer.from(v).toString('base64')\n\nconst https = require('https"
  },
  {
    "path": "packages/sharelist-core/lib/utils.js",
    "chars": 8343,
    "preview": "const crypto = require('crypto')\n\nconst { Transform } = require('node:stream')\n\nconst mimeParse = require('mime')\n\nconst"
  },
  {
    "path": "packages/sharelist-core/package.json",
    "chars": 629,
    "preview": "{\n  \"name\": \"@sharelist/core\",\n  \"version\": \"0.2.0\",\n  \"repository\": \"https://github.com/reruin/sharelist\",\n  \"license\":"
  },
  {
    "path": "packages/sharelist-core/test/index.js",
    "chars": 743,
    "preview": "const createDriver = require('../')\n\nconst path = require('path')\n\nconst testdriver = () => {\n  const list = (id) => {\n "
  },
  {
    "path": "packages/sharelist-core/test/plugin/driver.test.js",
    "chars": 547,
    "preview": "modules.export = () => {\n  const list = (id) => {\n    return {\n      id: 'folder',\n      files: new Array(10).fill(0).ma"
  },
  {
    "path": "packages/sharelist-manage/.eslintrc.js",
    "chars": 1004,
    "preview": "module.exports = {\n  parser: 'vue-eslint-parser',\n  parserOptions: {\n    // set script parser\n    parser: '@typescript-e"
  },
  {
    "path": "packages/sharelist-manage/.gitignore",
    "chars": 87,
    "preview": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\nyarn-error.log\npackage-lock.json\nyarn.lock"
  },
  {
    "path": "packages/sharelist-manage/.prettierrc.js",
    "chars": 273,
    "preview": "// https://prettier.io/docs/en/configuration.html\nmodule.exports = {\n  //分号终止符\n  semi: false,\n\n  //行尾逗号\n  trailingComma:"
  },
  {
    "path": "packages/sharelist-manage/CHANGELOG.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/sharelist-manage/README.md",
    "chars": 155,
    "preview": "# @sharelist/web [![npm](https://img.shields.io/npm/v/@sharelist/web.svg)](https://npmjs.com/package/@sharelist/web)\n\nIt"
  },
  {
    "path": "packages/sharelist-manage/package.json",
    "chars": 1441,
    "preview": "{\n  \"name\": \"@sharelist/manage\",\n  \"version\": \"0.2.0\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    "
  },
  {
    "path": "packages/sharelist-manage/src/App.tsx",
    "chars": 339,
    "preview": "import { defineComponent } from 'vue'\nimport { ConfigProvider } from 'ant-design-vue'\nimport { RouterView } from 'vue-ro"
  },
  {
    "path": "packages/sharelist-manage/src/assets/style/index.less",
    "chars": 27151,
    "preview": "// @import './icon.less';\n\n@import 'ant-design-vue/lib/style/variable.less';\n@import 'ant-design-vue/lib/button/style/in"
  },
  {
    "path": "packages/sharelist-manage/src/assets/style/var.less",
    "chars": 3051,
    "preview": "\n\n:root{\n  // --theme:100,52,248;\n  --theme:100,58,218;\n  --primary-color:rgb(var(--theme)); //rgb(105,65,199);\n  --prim"
  },
  {
    "path": "packages/sharelist-manage/src/components/code-editor/index.less",
    "chars": 491,
    "preview": "\n.cm-editor { max-height: 500px; border: 1px solid silver; font-size: 14px }\n.cm-scroller { overflow: auto; }\n.cm-conten"
  },
  {
    "path": "packages/sharelist-manage/src/components/code-editor/index.tsx",
    "chars": 999,
    "preview": "import './index.less'\nimport { ref, defineComponent, watch, onMounted, onUnmounted, toRefs, reactive, watchEffect } from"
  },
  {
    "path": "packages/sharelist-manage/src/components/icon/icon-svg.js",
    "chars": 32655,
    "preview": "!(function (t) {\n  var a,\n    l,\n    o,\n    i,\n    e,\n    c,\n    h =\n      '<svg><symbol id=\"icon-drive\" viewBox=\"0 0 10"
  },
  {
    "path": "packages/sharelist-manage/src/components/icon/index.less",
    "chars": 1085,
    "preview": "// .sl-icon {\n//   display: inline-block;\n//   font-style: normal;\n//   vertical-align: -0.125em;\n//   text-align: cente"
  },
  {
    "path": "packages/sharelist-manage/src/components/icon/index.ts",
    "chars": 230,
    "preview": "import { createFromIconfontCN } from '@ant-design/icons-vue'\nimport config from '../../config/setting'\nimport './icon-sv"
  },
  {
    "path": "packages/sharelist-manage/src/components/image/index.less",
    "chars": 31,
    "preview": ".hide-modal{\n  display: none;\n}"
  },
  {
    "path": "packages/sharelist-manage/src/components/image/index.tsx",
    "chars": 519,
    "preview": "import { Image, Modal } from 'ant-design-vue'\n\nexport const showImage = (urls: Array<string>, index: number) => {\n  cons"
  },
  {
    "path": "packages/sharelist-manage/src/components/modal/index.less",
    "chars": 203,
    "preview": ".hide--modal{\n  .ant-modal-confirm-btns,.anticon-exclamation-circle{\n    display: none;\n  }\n  .ant-modal-body{\n    paddi"
  },
  {
    "path": "packages/sharelist-manage/src/components/modal/index.tsx",
    "chars": 376,
    "preview": "import { Modal } from 'ant-design-vue'\nimport './index.less'\nexport const modal = function (options: any) {\n  const { co"
  },
  {
    "path": "packages/sharelist-manage/src/components/player/index.less",
    "chars": 5166,
    "preview": ".widget-player{\n  position: fixed;\n  bottom:0;\n  opacity: 0;\n  pointer-events: none;\n  // transform:translate(-50%,0);\n "
  },
  {
    "path": "packages/sharelist-manage/src/components/player/index.tsx",
    "chars": 5084,
    "preview": "import Plyr from 'plyr'\nimport { ref, reactive, defineComponent, onMounted, onUnmounted, computed, watch, watchEffect } "
  },
  {
    "path": "packages/sharelist-manage/src/components/sider/index.less",
    "chars": 1534,
    "preview": ".sider {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  width:64px;\n  .m"
  },
  {
    "path": "packages/sharelist-manage/src/components/sider/index.tsx",
    "chars": 2733,
    "preview": "import './index.less'\nimport { ref, defineComponent, computed, watch, onMounted, onUnmounted, toRefs, reactive, watchEff"
  },
  {
    "path": "packages/sharelist-manage/src/components.d.ts",
    "chars": 388,
    "preview": "// generated by unplugin-vue-components\n// We suggest you to commit this file into source control\n// Read more: https://"
  },
  {
    "path": "packages/sharelist-manage/src/config/api.ts",
    "chars": 2544,
    "preview": "const api: Array<unknown> = [\n  ['siginin', 'POST /signin'],\n  ['list', 'GET /api/drive/path/:path'],\n  ['setting', 'GET"
  },
  {
    "path": "packages/sharelist-manage/src/config/setting.ts",
    "chars": 87,
    "preview": "export default {\n  iconFontCN: 'https://at.alicdn.com/t/font_2637962_voky2m76mr.js',\n}\n"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useApi.ts",
    "chars": 5358,
    "preview": "import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'\nimport { effectScope, EffectScope, App, "
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useClipboard.ts",
    "chars": 1774,
    "preview": "import { watch, onUnmounted, ref, Ref } from 'vue'\n\ntype Type = 'file' | 'string'\n\nconst watchOnce = (v: any, cb: any) ="
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useConfirm.ts",
    "chars": 1199,
    "preview": "import { Modal, message } from 'ant-design-vue'\nimport { createVNode, VNode } from 'vue'\nimport { ExclamationCircleOutli"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useDirective.ts",
    "chars": 370,
    "preview": "import { VNode, withDirectives } from 'vue'\n\nconst focus = {\n  mounted: (el: any, { arg }: any) => {\n    el.focus()\n    "
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useDom.ts",
    "chars": 875,
    "preview": "import { ref, Ref } from 'vue'\n\nexport const isDocumentVisible = (): boolean => {\n  return document?.visibilityState !=="
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useHooks.ts",
    "chars": 4213,
    "preview": "import { ref, reactive, Ref, UnwrapRef, onMounted, onUnmounted, getCurrentInstance } from 'vue'\n\nexport function safeOnM"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useLoad.ts",
    "chars": 229,
    "preview": "import { ref, Ref } from 'vue'\n\nexport const useLoad = (cb: (() => Promise<any>) | Promise<any>): Ref<boolean> => {\n  co"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useLocalStorage.ts",
    "chars": 663,
    "preview": "import { Ref, ref, watch } from 'vue'\n\nexport type LocalStateKey = string\n\nexport function useLocalStorageState<T = any>"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useRequest.ts",
    "chars": 8365,
    "preview": "import { reactive, ref, Ref, toRefs, watch, onUnmounted } from 'vue'\n\nexport const useState = <T extends Record<string, "
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useScroll.ts",
    "chars": 2220,
    "preview": "import { ref, Ref, reactive, UnwrapRef } from 'vue'\n\ninterface RequestOptions<T, P> {\n  immediate?: boolean\n  isNoMore?:"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useSetting.ts",
    "chars": 7595,
    "preview": "import { ref, Ref, reactive } from 'vue'\nimport { useApi, ReqResponse } from '@/hooks/useApi'\nimport { message } from 'a"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useStore.ts",
    "chars": 2077,
    "preview": "import { provide, inject, Ref, ref } from 'vue'\n\ntype InjectType = 'root' | 'optional'\n\nexport interface FunctionalStore"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useUrlState.ts",
    "chars": 1113,
    "preview": "import { useRouter, useRoute } from 'vue-router'\nimport { reactive, watch } from 'vue'\nimport { useState } from './useHo"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/useWorker.ts",
    "chars": 1795,
    "preview": "const fnCache = new WeakMap()\n\ntype Fn = (...args: any[]) => any\n\nconst WORKER_SCRIPT = () => {\n  const methodsMap: Reco"
  },
  {
    "path": "packages/sharelist-manage/src/hooks/utils.ts",
    "chars": 423,
    "preview": "import { getCurrentInstance, isRef, onMounted as vueOnMounted, onUnmounted as vueOnUnmounted, Ref } from 'vue'\n\nexport c"
  },
  {
    "path": "packages/sharelist-manage/src/index.html",
    "chars": 358,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <link rel=\"icon\" href=\"/favicon.ico\" />\n  <meta na"
  },
  {
    "path": "packages/sharelist-manage/src/main.ts",
    "chars": 1250,
    "preview": "import { createApp, h } from 'vue'\nimport App from './App'\nimport router from './router'\nimport { message, Spin, ConfigP"
  },
  {
    "path": "packages/sharelist-manage/src/router/index.ts",
    "chars": 980,
    "preview": "import { createRouter, createWebHashHistory, createWebHistory, RouteRecordRaw, onBeforeRouteLeave } from 'vue-router'\n\nc"
  },
  {
    "path": "packages/sharelist-manage/src/store/index.ts",
    "chars": 626,
    "preview": "import { defineStore } from 'pinia'\n\nexport default defineStore('sharelist_manage', {\n  state: () => ({\n    accessToken:"
  },
  {
    "path": "packages/sharelist-manage/src/types/IDrive.ts",
    "chars": 873,
    "preview": "type DrivePath = {\n  protocol: string\n  [key: string]: string | number\n}\n\ndeclare type DriverField = {\n  key: string\n  l"
  },
  {
    "path": "packages/sharelist-manage/src/types/shim.d.ts",
    "chars": 268,
    "preview": "declare module '*.vue' {\n  import { DefineComponent } from 'vue'\n  const component: DefineComponent<{}, {}, any>\n  expor"
  },
  {
    "path": "packages/sharelist-manage/src/types/source.d.ts",
    "chars": 107,
    "preview": "declare module '*.json'\ndeclare module '*.png'\ndeclare module '*.jpg'\ndeclare module 'vue-infinite-scroll'\n"
  },
  {
    "path": "packages/sharelist-manage/src/utils/format.ts",
    "chars": 4429,
    "preview": "const EXT_IMAGE = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 'tif', 'svg', 'webp']\n\nconst EXT_AUDIO_SUPPORT = ["
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/index.less",
    "chars": 6679,
    "preview": ".drive{\n  display: flex;\n  flex-direction: column;\n  height:100vh;\n\n  &.drive--lite{\n    height:auto;\n  }\n  .drive__head"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/index.tsx",
    "chars": 22915,
    "preview": "import { h, ref, Ref, defineComponent, watch, onMounted, onUnmounted, nextTick, toRef, computed, withModifiers } from 'v"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/action/index.less",
    "chars": 960,
    "preview": ".file-item{\n  padding-right:8px;\n  overflow-x: hidden;\n  .ant-checkbox-inner{\n    border-radius: 50%;\n    box-sizing: bo"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/action/index.tsx",
    "chars": 18784,
    "preview": "import { defineComponent, withDirectives, ref, getCurrentInstance, Ref, computed, reactive } from \"vue\";\nimport { Input,"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/auth/index.less",
    "chars": 480,
    "preview": ".auth-box{\n  height:70vh;\n  width:100%;\n  display:flex;\n  align-items:center;\n  justify-content:center;\n  flex-direction"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/auth/index.tsx",
    "chars": 1447,
    "preview": "import { ref, defineComponent, watch, onMounted, toRef, watchEffect, reactive } from 'vue'\nimport { Button, Input } from"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/breadcrumb/index.less",
    "chars": 761,
    "preview": ".drive-breadcrumb{\n  a{\n    // color:var(--primary-color);\n    font-weight: 600;\n    max-width: 100%;\n    overflow: hidd"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/breadcrumb/index.tsx",
    "chars": 3606,
    "preview": "import { ref, defineComponent, reactive, onMounted, onUnmounted, computed, watchEffect, PropType, watch } from 'vue'\nimp"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/error/index.less",
    "chars": 451,
    "preview": "\n.err{\n  height:70vh;\n  width:100%;\n  display:flex;\n  align-items:center;\n  justify-content:center;\n  flex-direction:col"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/error/index.tsx",
    "chars": 837,
    "preview": "import { defineComponent, watch } from 'vue'\nimport './index.less'\nimport { message } from 'ant-design-vue'\nimport AuthB"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/meta/index.less",
    "chars": 969,
    "preview": ".file-meta{\n  display: flex;\n  align-items: center;\n\n  .item-thumb{\n    width:60px;height:40px;\n    border-radius: 6px;\n"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/meta/index.tsx",
    "chars": 5901,
    "preview": "import { defineComponent, withModifiers, ref } from \"vue\";\nimport Icon from '@/components/icon'\nimport useDisk from '../"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/modifier/index.less",
    "chars": 153,
    "preview": ".modifier{\n  .modifier__footer{\n    padding:0 16px;\n    display: flex;\n    align-items: center;\n    justify-content: spa"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/modifier/index.tsx",
    "chars": 5865,
    "preview": "import { ref, Ref, defineComponent, watchEffect, toRaw, reactive, UnwrapRef, watch } from 'vue'\nimport { useSetting } fr"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/search/index.less",
    "chars": 36,
    "preview": ".search-box{\n  position: absolute;\n}"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/search/index.tsx",
    "chars": 1115,
    "preview": "import { ref, defineComponent, watch, onMounted } from 'vue'\nimport { InputSearch, RadioGroup, Radio } from 'ant-design-"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/task/index.less",
    "chars": 2291,
    "preview": ".task{\n  width: 375px;\n  // background-color:var(--context-background);\n  \n  border-radius: 4px;\n  // box-shadow: 0 6px "
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/task/index.tsx",
    "chars": 11058,
    "preview": "import { defineComponent, onUnmounted } from \"vue\"\nimport { useApi } from '@/hooks/useApi'\nimport { useRequest } from '@"
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/upload/index.tsx",
    "chars": 11484,
    "preview": "\nimport { defineComponent } from 'vue'\nimport { useApi, ReqResponse } from '@/hooks/useApi'\nimport { reactive, ref, Ref "
  },
  {
    "path": "packages/sharelist-manage/src/views/disk/partial/useDisk.ts",
    "chars": 8714,
    "preview": "import { useApi, ReqResponse } from '@/hooks/useApi'\nimport { ref, Ref, watch, reactive, computed, inject, provide, getC"
  },
  {
    "path": "packages/sharelist-manage/src/views/general/index.less",
    "chars": 1036,
    "preview": ".page--setting {\n  // max-width:960px;\n\n  padding:32px;\n  .setting-drive__header {\n    text-align: right;\n  }\n\n  .item {"
  },
  {
    "path": "packages/sharelist-manage/src/views/general/index.tsx",
    "chars": 7294,
    "preview": "import { ref, defineComponent, watch, onMounted, toRef, toRefs, reactive } from 'vue'\nimport { RadioGroup, Radio, messag"
  },
  {
    "path": "packages/sharelist-manage/src/views/home/index.less",
    "chars": 387,
    "preview": "\n.layout{\n  display: flex;\n  height: 100vh;\n  // background: var(--background_secondary);\n\n    // padding:30px;\n  // bor"
  },
  {
    "path": "packages/sharelist-manage/src/views/home/index.tsx",
    "chars": 782,
    "preview": "import Sider from '../../components/sider'\nimport { RouterView } from 'vue-router'\nimport { defineComponent } from 'vue'"
  },
  {
    "path": "packages/sharelist-manage/src/views/plugin/index.less",
    "chars": 1548,
    "preview": ".page--plugin{\n  padding:32px;\n\n  .page-content{\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax("
  },
  {
    "path": "packages/sharelist-manage/src/views/plugin/index.tsx",
    "chars": 6972,
    "preview": "import { ref, defineComponent, watch, onMounted, toRef, toRefs, reactive, watchEffect } from 'vue'\nimport Icon from '@/c"
  },
  {
    "path": "packages/sharelist-manage/src/views/plugin/partial/store/index.less",
    "chars": 1376,
    "preview": ".plugin-store{\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n  grid-gap: 12px;\n  heig"
  },
  {
    "path": "packages/sharelist-manage/src/views/plugin/partial/store/index.tsx",
    "chars": 3746,
    "preview": "import { ref, defineComponent, watch, onMounted, toRef, toRefs, reactive, watchEffect, Ref, computed } from 'vue'\nimport"
  },
  {
    "path": "packages/sharelist-manage/src/views/signin/index.less",
    "chars": 854,
    "preview": ".page-signin {\n\n\n  .page-signin-wrap{\n    position: fixed;\n    top: 45%;\n    left: 50%;\n    transform: translate(-50%, -"
  },
  {
    "path": "packages/sharelist-manage/src/views/signin/index.tsx",
    "chars": 971,
    "preview": "import { ref, defineComponent } from 'vue'\nimport { Button, Input } from 'ant-design-vue'\nimport { useSetting } from '@/"
  },
  {
    "path": "packages/sharelist-manage/src/views/test/index.vue",
    "chars": 428,
    "preview": "<template>\n  <h1>Home</h1>\n  <nav><router-link to=\"/plugin\">plugin</router-link> |</nav>\n</template>\n\n<script lang=\"ts\">"
  },
  {
    "path": "packages/sharelist-manage/tsconfig.json",
    "chars": 792,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"strict\": t"
  },
  {
    "path": "packages/sharelist-manage/vite.config.ts",
    "chars": 1323,
    "preview": "import { defineConfig } from 'vite'\nimport vueJsx from '@vitejs/plugin-vue-jsx'\nimport legacy from '@vitejs/plugin-legac"
  },
  {
    "path": "packages/sharelist-web/.eslintrc.js",
    "chars": 1004,
    "preview": "module.exports = {\n  parser: 'vue-eslint-parser',\n  parserOptions: {\n    // set script parser\n    parser: '@typescript-e"
  },
  {
    "path": "packages/sharelist-web/.gitignore",
    "chars": 87,
    "preview": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\nyarn-error.log\npackage-lock.json\nyarn.lock"
  },
  {
    "path": "packages/sharelist-web/.prettierrc.js",
    "chars": 273,
    "preview": "// https://prettier.io/docs/en/configuration.html\nmodule.exports = {\n  //分号终止符\n  semi: false,\n\n  //行尾逗号\n  trailingComma:"
  },
  {
    "path": "packages/sharelist-web/CHANGELOG.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/sharelist-web/README.md",
    "chars": 155,
    "preview": "# @sharelist/web [![npm](https://img.shields.io/npm/v/@sharelist/web.svg)](https://npmjs.com/package/@sharelist/web)\n\nIt"
  },
  {
    "path": "packages/sharelist-web/package.json",
    "chars": 1144,
    "preview": "{\n  \"name\": \"@sharelist/web\",\n  \"version\": \"0.2.0\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"ch"
  },
  {
    "path": "packages/sharelist-web/src/App.tsx",
    "chars": 339,
    "preview": "import { defineComponent } from 'vue'\nimport { ConfigProvider } from 'ant-design-vue'\nimport { RouterView } from 'vue-ro"
  },
  {
    "path": "packages/sharelist-web/src/assets/style/index.less",
    "chars": 25479,
    "preview": "// @import './icon.less';\n@import './var.less';\n\n@import 'ant-design-vue/lib/style/variable.less';\n@import 'ant-design-v"
  },
  {
    "path": "packages/sharelist-web/src/assets/style/var.less",
    "chars": 2368,
    "preview": "\n\n:root{\n  // --theme:100,52,248;\n  --theme:24,144,255;\n  --primary-color:rgb(var(--theme)); //rgb(105,65,199);\n  --prim"
  },
  {
    "path": "packages/sharelist-web/src/components/icon/icon-svg.js",
    "chars": 32655,
    "preview": "!(function (t) {\n  var a,\n    l,\n    o,\n    i,\n    e,\n    c,\n    h =\n      '<svg><symbol id=\"icon-drive\" viewBox=\"0 0 10"
  },
  {
    "path": "packages/sharelist-web/src/components/icon/index.less",
    "chars": 1095,
    "preview": "// .sl-icon {\n//   display: inline-block;\n//   font-style: normal;\n//   vertical-align: -0.125em;\n//   text-align: cente"
  },
  {
    "path": "packages/sharelist-web/src/components/icon/index.ts",
    "chars": 230,
    "preview": "import { createFromIconfontCN } from '@ant-design/icons-vue'\nimport config from '../../config/setting'\nimport './icon-sv"
  },
  {
    "path": "packages/sharelist-web/src/components/image/index.tsx",
    "chars": 531,
    "preview": "import { Image, Modal } from 'ant-design-vue'\n\nexport const showImage = (urls: Array<string>, index: number) => {\n  cons"
  },
  {
    "path": "packages/sharelist-web/src/components/player/index.less",
    "chars": 5093,
    "preview": ".widget-player{\n  position: fixed;\n  bottom:0;\n  opacity: 0;\n  pointer-events: none;\n  // transform:translate(-50%,0);\n "
  },
  {
    "path": "packages/sharelist-web/src/components/player/index.tsx",
    "chars": 4790,
    "preview": "import Plyr from 'plyr'\nimport { ref, reactive, defineComponent, onMounted, onUnmounted, computed, watch, watchEffect } "
  },
  {
    "path": "packages/sharelist-web/src/config/api.ts",
    "chars": 488,
    "preview": "export type IAPI = [\n  name: string,\n  url: string | ((...args: any[]) => string),\n  options?: {\n    [key: string]: numb"
  },
  {
    "path": "packages/sharelist-web/src/config/setting.ts",
    "chars": 18,
    "preview": "export default {}\n"
  },
  {
    "path": "packages/sharelist-web/src/hooks/useApi.ts",
    "chars": 5358,
    "preview": "import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'\nimport { effectScope, EffectScope, App, "
  },
  {
    "path": "packages/sharelist-web/src/hooks/useDisk.ts",
    "chars": 8328,
    "preview": "import { useApi, ReqResponse } from '@/hooks/useApi'\nimport { ref, Ref, watch, reactive, computed, inject, provide, getC"
  },
  {
    "path": "packages/sharelist-web/src/hooks/useHooks.ts",
    "chars": 4160,
    "preview": "import { ref, reactive, Ref, UnwrapRef, onMounted, onUnmounted, getCurrentInstance } from 'vue'\n\nexport function safeOnM"
  },
  {
    "path": "packages/sharelist-web/src/hooks/useLocalStorage.ts",
    "chars": 672,
    "preview": "import { Ref, ref, watch } from 'vue'\n\nexport type LocalStateKey = string\n\nexport function useLocalStorageState<T = any>"
  },
  {
    "path": "packages/sharelist-web/src/hooks/useScroll.ts",
    "chars": 2093,
    "preview": "import { ref, Ref, reactive, UnwrapRef } from 'vue'\n\ninterface RequestOptions<T, P> {\n  immediate?: boolean\n  isNoMore?:"
  },
  {
    "path": "packages/sharelist-web/src/hooks/useSetting.ts",
    "chars": 835,
    "preview": "import { ref, Ref, reactive, unref, readonly, toRaw } from 'vue'\nimport { useApi, ReqResponse } from '@/hooks/useApi'\n\nt"
  },
  {
    "path": "packages/sharelist-web/src/hooks/utils.ts",
    "chars": 423,
    "preview": "import { getCurrentInstance, isRef, onMounted as vueOnMounted, onUnmounted as vueOnUnmounted, Ref } from 'vue'\n\nexport c"
  },
  {
    "path": "packages/sharelist-web/src/index.html",
    "chars": 689,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <link rel=\"icon\" href=\"/favicon.ico\" />\n  <meta na"
  },
  {
    "path": "packages/sharelist-web/src/main.ts",
    "chars": 876,
    "preview": "import { createApp, h } from 'vue'\nimport App from './App'\nimport store from './store'\nimport router from './router'\nimp"
  },
  {
    "path": "packages/sharelist-web/src/router/index.ts",
    "chars": 356,
    "preview": "import { defineAsyncComponent } from 'vue'\nimport { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'\n\n"
  },
  {
    "path": "packages/sharelist-web/src/store/index.ts",
    "chars": 285,
    "preview": "import { defineStore } from 'pinia'\n\nexport default defineStore('main', {\n  state: () => ({\n    theme: 'light',\n    layo"
  },
  {
    "path": "packages/sharelist-web/src/types/IDrive.ts",
    "chars": 655,
    "preview": "type DrivePath = {\n  protocol: string\n  [key: string]: string | number\n}\n\ndeclare type DriverField = {\n  key: string\n  l"
  },
  {
    "path": "packages/sharelist-web/src/types/IRequest.ts",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/sharelist-web/src/types/shim.d.ts",
    "chars": 344,
    "preview": "declare module '*.vue' {\n  import { DefineComponent } from 'vue'\n  const component: DefineComponent<{}, {}, any>\n  expor"
  },
  {
    "path": "packages/sharelist-web/src/types/source.d.ts",
    "chars": 107,
    "preview": "declare module '*.json'\ndeclare module '*.png'\ndeclare module '*.jpg'\ndeclare module 'vue-infinite-scroll'\n"
  },
  {
    "path": "packages/sharelist-web/src/utils/format.ts",
    "chars": 3854,
    "preview": "const EXT_IMAGE = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 'tif', 'svg', 'webp']\n\nconst EXT_AUDIO_SUPPORT = ["
  },
  {
    "path": "packages/sharelist-web/src/views/home/index.less",
    "chars": 4594,
    "preview": ".drive-search{\n  max-width:560px;\n  width:90%;\n  margin: auto;\n  transform: translate(0,20vh);\n}\n.drive-body{\n  padding:"
  },
  {
    "path": "packages/sharelist-web/src/views/home/index.tsx",
    "chars": 5085,
    "preview": "import { onMounted, onUnmounted, defineComponent, ref, reactive, toRef, watch, withModifiers } from 'vue'\nimport { Spin,"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/auth/index.less",
    "chars": 480,
    "preview": ".auth-box{\n  height:70vh;\n  width:100%;\n  display:flex;\n  align-items:center;\n  justify-content:center;\n  flex-direction"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/auth/index.tsx",
    "chars": 1486,
    "preview": "import { ref, defineComponent, watch, onMounted, toRef, watchEffect, reactive } from 'vue'\nimport { Button, Input } from"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/breadcrumb/index.less",
    "chars": 782,
    "preview": ".drive-breadcrumb{\n  padding:12px 40px;\n  a{\n    // color:var(--primary-color);\n    font-weight: 600;\n    max-width: 100"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/breadcrumb/index.tsx",
    "chars": 3629,
    "preview": "import { ref, defineComponent, reactive, onMounted, onUnmounted, computed, watchEffect, PropType, watch } from 'vue'\nimp"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/error/index.less",
    "chars": 451,
    "preview": "\n.err{\n  height:70vh;\n  width:100%;\n  display:flex;\n  align-items:center;\n  justify-content:center;\n  flex-direction:col"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/error/index.tsx",
    "chars": 837,
    "preview": "import { defineComponent, watch } from 'vue'\nimport './index.less'\nimport { message } from 'ant-design-vue'\nimport AuthB"
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/header/index.less",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/sharelist-web/src/views/home/partial/header/index.tsx",
    "chars": 4791,
    "preview": "import { ref, defineComponent, watch, onMounted } from 'vue'\nimport './index.less'\nimport Icon from '@/components/icon'\n"
  },
  {
    "path": "packages/sharelist-web/tsconfig.json",
    "chars": 792,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"strict\": t"
  },
  {
    "path": "packages/sharelist-web/vite.config.ts",
    "chars": 1181,
    "preview": "import { defineConfig } from 'vite'\nimport vueJsx from '@vitejs/plugin-vue-jsx'\nimport legacy from '@vitejs/plugin-legac"
  },
  {
    "path": "packages/sharelist-webdav/CHANGELOG.md",
    "chars": 1070,
    "preview": "## [0.1.11](https://github.com/reruin/sharelist/compare/v0.3.13...v0.1.11) (2022-01-05)\n\n\n### Bug Fixes\n\n* adapt webdav "
  }
]

// ... and 26 more files (download for full content)

About this extraction

This page contains the full source code of the reruin/sharelist GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 226 files (589.4 KB), approximately 203.9k tokens, and a symbol index with 285 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!