├── .commitlintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTION.md ├── CONTRIBUTION_en.md ├── EXPLANATION.md ├── EXPLANATION_en.md ├── GITFLOW.md ├── GITFLOW_en.md ├── LICENSE.txt ├── README.md ├── README_en.md ├── build ├── common.ts ├── dev.ts ├── lib.ts └── test.ts ├── eslint.config.js ├── examples ├── .vscode │ ├── settings.json │ └── tailwindcss.json ├── auto-imports.d.ts ├── build │ └── resolvers │ │ └── antd.ts ├── eslint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src │ ├── App.css │ ├── App.tsx │ ├── components │ │ └── Uploader │ │ │ └── index.tsx │ ├── index.css │ ├── main.tsx │ ├── plugins │ │ ├── index.ts │ │ └── unocss.ts │ ├── styles │ │ ├── index.css │ │ └── normalize.css │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── unocss.config.ts └── vite.config.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── server ├── app.js ├── bin │ └── www ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public │ └── stylesheets │ │ └── style.css ├── routes │ └── index.js └── views │ ├── error.pug │ ├── index.pug │ └── layout.pug ├── src ├── main.ts ├── packages │ ├── constant.ts │ ├── error │ │ └── index.ts │ ├── hooks │ │ ├── index.ts │ │ ├── types.ts │ │ ├── useControl │ │ │ └── index.ts │ │ ├── useNextTick │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── useStoreTerminable │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── index.ts │ ├── modules │ │ └── handles │ │ │ ├── clearRuntimeProduct.ts │ │ │ ├── createChunks.ts │ │ │ ├── createHash │ │ │ ├── index.ts │ │ │ ├── shared.ts │ │ │ └── worker │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── index.ts │ ├── shared │ │ └── index.ts │ └── types.ts ├── style.css ├── upload.ts └── vite-env.d.ts ├── tests └── client │ ├── fileUpload.spec.ts │ └── mock.ts ├── tsconfig.build.json ├── tsconfig.json ├── vitest.config.mts └── vitest.workspace.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ], 5 | "rules": { 6 | "type-enum": [ 7 | 2, 8 | "always", 9 | [ 10 | "feat", 11 | "fix", 12 | "docs", 13 | "style", 14 | "refactor", 15 | "test", 16 | "chore", 17 | "revert" 18 | ] 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: 'PR and push' 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | All-Test: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Install PNPM 20 | uses: pnpm/action-setup@v4 21 | with: 22 | version: 10 23 | run_install: false 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Test 35 | run: pnpm test --coverage.enabled true 36 | 37 | - name: Report Coverage 38 | # Set if: always() to also generate the report if tests are failing 39 | # Only works if you set `reportOnFailure: true` in your vite config as specified above 40 | if: always() 41 | uses: davelosert/vitest-coverage-report-action@v2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | /coverage/ 11 | 12 | dist 13 | dist-ssr 14 | *.local 15 | **/node_modules/** 16 | 17 | # Editor directories and files 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm dlx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint 2 | git add . 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 版本控制系统相关 2 | .git 3 | .gitignore 4 | 5 | # 构建输出目录(根据你的构建工具调整) 6 | /build 7 | /lib 8 | /out-tsc 9 | 10 | # 测试文件 11 | /test 12 | /tests 13 | /spec 14 | /__tests__ 15 | *.test.js 16 | *.spec.js 17 | *.test.ts 18 | *.spec.ts 19 | 20 | # 开发环境配置及依赖 21 | /node_modules 22 | /.vscode 23 | /.idea 24 | 25 | # 本地服务器 26 | /server 27 | 28 | # IDE 配置文件 29 | *.sublime-project 30 | *.sublime-workspace 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | 37 | # 日志文件 38 | logs 39 | *.log 40 | npm-debug.log* 41 | yarn-debug.log* 42 | yarn-error.log* 43 | pnpm-debug.log* 44 | lerna-debug.log* 45 | 46 | # 本地环境配置 47 | .env 48 | .env.local 49 | .env.development 50 | .env.test 51 | .env.production 52 | 53 | # 敏感数据与临时文件 54 | /secrets 55 | /tmp 56 | /temp 57 | 58 | # 文档与资源 59 | /docs 60 | /example 61 | /sample 62 | /readme.md~ 63 | *.md~ 64 | 65 | # 示例 66 | /examples 67 | 68 | # 特定于操作系统的文件 69 | Thumbs.db 70 | .DS_Store 71 | 72 | # 其他配置和生成文件 73 | .npmrc 74 | eslint.config.js 75 | package-lock.json 76 | yarn.lock 77 | pnpm-lock.yaml 78 | public 79 | index.html 80 | 81 | # Husky hooks 目录 82 | .husky 83 | 84 | # 源代码目录(如果你不希望发布源代码) 85 | /src 86 | 87 | # 忽略所有 .local 文件 88 | *.local 89 | 90 | # 其他文件 91 | /coverage 92 | .commitlintrc.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # registry=https://registry.npmmirror.com/ 2 | registry=https://registry.npmjs.org/ 3 | shamefully-hoist=true 4 | ignore-workspace-root-check=true 5 | link-workspace-packages=true -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "russell.any-rule", 5 | "rvest.vs-code-prettier-eslint", 6 | "YoavBls.pretty-ts-errors", 7 | "vitest.explorer", 8 | "crystal-spider.jsdoc-generator", 9 | "ritwickdey.LiveServer", 10 | "antfu.unocss", 11 | "esbenp.prettier-vscode" 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", 3 | "editor.formatOnPaste": false, 4 | "editor.formatOnType": false, 5 | "editor.formatOnSave": true, 6 | "editor.formatOnSaveMode": "file", 7 | "files.autoSave": "onFocusChange", 8 | "vs-code-prettier-eslint.prettierLast": true, 9 | "[vue]": { 10 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 11 | }, 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "always", 14 | "source.fixAll.ts": "always" 15 | }, 16 | "eslint.validate": [ 17 | "javascript", 18 | "javascriptreact", 19 | "typescript", 20 | "typescriptreact", 21 | "html" 22 | ], 23 | "eslint.probe": [ 24 | "javascript", 25 | "javascriptreact", 26 | "typescript", 27 | "typescriptreact", 28 | "html" 29 | ], 30 | "css.validate": true, 31 | "scss.validate": true, 32 | "[typescript]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "files.eol": "\n", 36 | "[javascript]": { 37 | "editor.defaultFormatter": "esbenp.prettier-vscode" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | ## 贡献指南 2 | 3 | 感谢你对本项目感兴趣并愿意贡献!在开始之前,请仔细阅读以下指南。 4 | 5 | [基本信息](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/EXPLANATION.md) 6 | 7 | [Git Flow](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/GITFLOW.md) 8 | 9 | ### 1. Fork 仓库 10 | 11 | 点击页面右上角的 "Fork" 按钮来创建这个项目的个人副本。这样你就可以自由修改而不会影响原始项目。 12 | 13 | ### 2. Clone 你的 Fork 14 | 15 | 使用命令行工具将你的 fork 克隆到本地机器: 16 | 17 | ``` 18 | git clone https://github.com/YTXEternal/ux-plus-chunk-uploader.git 19 | ``` 20 | 21 | ### 4. 编写代码或文档 22 | 23 | 根据需要添加、编辑或删除代码或文档。请遵循项目内的编码规范(如果有的话)。 24 | 25 | ### 5. 编写你的单元测试 26 | 27 | 确保所有测试都通过,并且没有引入新的问题。如果适用,增加新的测试来验证你的更改。 28 | 29 | ### 6. 提交更改 30 | 31 | 提交你的更改,并提供清晰的提交信息说明你做了什么以及为什么这样做。 32 | 33 | ``` 34 | pnpm commit 35 | ``` 36 | 37 | ### 7. Push 到你的 Fork 38 | 39 | 将你的分支推送到 GitHub 上的 fork 中 40 | 41 | ### 8. 发起 Pull Request 42 | 43 | 访问原项目的 GitHub 页面,你应该能看到一个提示框邀请你发起一个 pull request。点击它,并按照指示完成操作。 -------------------------------------------------------------------------------- /CONTRIBUTION_en.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guideline 2 | 3 | Thank you for your interest in this project and willingness to contribute! In the beginning, please read the following guidelines carefully. 4 | 5 | [basic info](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/EXPLANATION_en.md) 6 | 7 | [Git Flow](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/GITFLOW_en.md) 8 | 9 | ### 1. Fork 10 | 11 | Click the "Fork" button at the top right corner of the page to create a personal copy of this project. This way, you can make changes freely without affecting the original project. 12 | 13 | ### 2. Clone your Fork 14 | 15 | Use the command line tool to clone your fork to your local machine: 16 | 17 | ```cmd 18 | git clone https://github.com/YTXEternal/ux-plus-chunk-uploader.git 19 | ``` 20 | 21 | ### 4. Write code or documentation 22 | 23 | Add, edit, or delete code or documentation as needed. Please follow any coding conventions (if applicable) within the project. 24 | 25 | ### 5. Write your unit tests 26 | 27 | Ensure all tests pass and no new issues have been introduced. If applicable, add new tests to verify your changes. 28 | 29 | ### 6. Commit changes 30 | 31 | Ensure all tests pass and no new issues have been introduced. If applicable, add new tests to verify your changes. 32 | 33 | ```cmd 34 | pnpm commit 35 | ``` 36 | 37 | ### 7. Push to your fork 38 | 39 | Push your branch to the fork on GitHub. 40 | 41 | ### 8. Create a pull request 42 | 43 | Visit the original project's GitHub page, where you should see a prompt inviting you to create a pull request. Click it and follow the instructions to complete the process. -------------------------------------------------------------------------------- /EXPLANATION.md: -------------------------------------------------------------------------------- 1 | ## 项目文件解释 2 | 3 | ### build 4 | 5 | 项目构建配置 6 | 7 | common.ts 公共配置 8 | 9 | dev.ts 开发配置 10 | 11 | lib.ts 生产配置 12 | 13 | test.ts 测试配置 14 | 15 | 16 | 17 | ### server 18 | 19 | 本地实际测试api接口(在项目开发时需要启动这个服务) 20 | 21 | npm install 22 | 23 | 运行 24 | 25 | npm run dev 26 | 27 | 28 | 29 | ### src/packages 30 | 31 | 项目源文件 32 | 33 | 34 | 35 | ### src/main.ts,src/upload.ts 36 | 37 | 项目开发时的调试文件 38 | 39 | 40 | 41 | ### examples 42 | 43 | 在打包dist后进行生产测试使用pnpm link连接dist打包后的产物导入项目中使用 44 | 45 | 46 | 47 | ### tests 48 | 49 | 基于vitest进行单元测试 50 | 51 | 52 | 53 | ## 启动项目 54 | 55 | 基于项目根目录下 56 | 57 | 前端 58 | 59 | ``` 60 | pnpm install 61 | pnpm dev 62 | ``` 63 | 64 | 65 | 66 | 后端服务 67 | 68 | ``` 69 | cd server 70 | npm install 71 | ``` 72 | 73 | 74 | 75 | 注:该项目基于commitizen进行规范化提交代码请严格按照提示进行提交以及 gitflow工作流来进行开发,谢谢 -------------------------------------------------------------------------------- /EXPLANATION_en.md: -------------------------------------------------------------------------------- 1 | ## Project File Explanation 2 | 3 | ### build 4 | 5 | Project build configuration 6 | 7 | common.ts Public configuration 8 | 9 | dev.ts Development configuration 10 | 11 | lib.ts Production configuration 12 | 13 | test.ts Testing configuration 14 | 15 | 16 | 17 | ### server 18 | 19 | Local actual test API interfaces (this service needs to be started during project development) 20 | 21 | ```cmd 22 | npm install 23 | 24 | run: 25 | 26 | npm run dev 27 | ``` 28 | 29 | 30 | 31 | ### src/packages 32 | 33 | Project source files 34 | 35 | 36 | 37 | ### src/main.ts,src/upload.ts 38 | 39 | Debugging files during project development 40 | 41 | 42 | 43 | ### examples 44 | 45 | Used for production testing after packaging into dist. Connect the output of dist using pnpm link and import it into the project for use. 46 | 47 | 48 | 49 | ### tests 50 | 51 | Unit testing based on vitest. 52 | 53 | 54 | 55 | ## boot project 56 | 57 | From the root directory of the project: 58 | 59 | Front End 60 | 61 | ``` 62 | pnpm install 63 | pnpm dev 64 | ``` 65 | 66 | 67 | 68 | Backend Service 69 | 70 | ``` 71 | cd server 72 | npm install 73 | ``` 74 | 75 | 76 | 77 | Note: This project is based on commitizen for standardized code submission; please follow the prompts strictly for submissions as well as use the gitflow workflow for development. Thank you. 78 | 79 | -------------------------------------------------------------------------------- /GITFLOW.md: -------------------------------------------------------------------------------- 1 | **把项目(clone)下载到本地** 2 | 3 | 4 | 5 | ## 初始化工作流 6 | 7 | ```cmd 8 | git flow init 9 | ``` 10 | 11 | 12 | 13 | 14 | ## master 15 | 16 | 不要直接在这个分支进行修改,也不要直接推送这个master分支 17 | 18 | 当你需要修改BUG的时候可以基于这个分支创建新的HOTFIX/*分支进行BUG的修改 19 | 20 | 21 | 22 | 23 | ## develop 24 | 25 | **基于master分支创建。** 26 | 27 | 如果本地已有develop分支在编码前拉取合并一下最新的develop分支 28 | ```cmd 29 | 确保当前是在develop分支 30 | git checkout develop 31 | 拉取githu进行更新 32 | git pull origin develop 33 | ``` 34 | 35 | 如果没有则自己创建一个develop分支 36 | ```cmd 37 | git checkout -b develop 38 | ``` 39 | 注:这个分支不要直接进行修改当你需要增加新功能时可以基于这个develop分支创建新的feature/*分支来进行开发 40 | 41 | 42 | 43 | 44 | ## feature/* 45 | ***代表一个任意的名字要符合你开发或者修改的相关内容** 46 | 47 | 基于develop分支创建 48 | 49 | 这个分支用来进行开发新功能 50 | 51 | 比如我要开发一个getData函数那就创建一个feature/getData分支在这个分支上进行开发就好了 52 | 53 | ```cmd 54 | 这个命令会创建 feature/getData分支 55 | git flow feature start getData 56 | 57 | 58 | ... 59 | 60 | 当你的功能完成后并且进行了实际的测试以及单元测试后就可以进行提交了 61 | pnpm commit 62 | 把你的feature分支推送到github中 等待我的审批就好了,当你的提交审核通过后就可以继续下面的操作了 63 | git push origin feature/getData 64 | (审核通过后继续操作) 65 | git flow feature finish getData // 这个操作会直接合并到develop分支这个功能分支 66 | ``` 67 | 68 | 69 | 70 | ## hotfix/* 71 | 72 | ***版本号 如v3.0.2** 73 | 74 | 基于master分支创建 75 | 76 | 这个分支用来修改bug 77 | 78 | ```cmd 79 | git flow hotfix start v3.0.2 // 这个命令会创建 hotfix/v3.0.2分支 80 | 81 | ... 82 | 83 | 修复完毕后 84 | pnpm commit 85 | 把这个分支推送到github中(在这之前记得做实际的测试以及单元测试) 86 | git push origin hotfix/v3.0.2 87 | 88 | 等待我审核通过你的提交后在继续下面的操作 89 | 合并回 master 和 develop 分支,并打上版本标签 90 | git flow hotfix finish v3.0.2 91 | git push origin v3.0.2 92 | ``` 93 | 94 | 95 | 96 | ## release/* 97 | 98 | ***版本号 如v3.0.0** 99 | 100 | 基于develop分支创建 101 | 102 | 这个分支用来发布新的版本 103 | 104 | develop分支开发完毕后如果需要发布新版本可以创建一个新的release分支比如v3.0.0 105 | 106 | ```cmd 107 | git flow release start v3.0.0 // 这个命令会创建 release/v3.0.0分支 108 | 109 | ... 110 | 111 | 开发完毕 112 | git push origin release/v3.0.0 113 | 114 | // 等待审核通过后继续操作 115 | git flow release finish v3.0.0 116 | git push origin v3.0.0 117 | ``` 118 | 119 | 120 | 121 | ## chore/* 122 | 123 | ***代表一个任意的名字要符合你开发或者修改的相关内容** 124 | 125 | 基于develop分支进行创建 126 | 127 | 这个分支用来增加文档或者纠正文档或者删除多余依赖等等杂物 128 | 129 | ```cmd 130 | 创建 131 | git checkout -b chore/* 132 | 133 | ... 134 | 135 | 这个分支完成后: 136 | 提交一下更新的内容(注意根据提示来书写内容) 137 | pnpm commit 138 | 把分支推送到github由我来审核如果通过则继续以下操作 139 | git push origin chore/* 140 | 141 | 通过后继续操作 142 | git checkout develop 143 | 先拉取一遍以保证是最新的代码 144 | git pull origin develop 145 | git merge chore/* 146 | ``` 147 | -------------------------------------------------------------------------------- /GITFLOW_en.md: -------------------------------------------------------------------------------- 1 | **Clone the project to your local machine** 2 | 3 | ## Initialize Workflow 4 | 5 | ```cmd 6 | git flow init 7 | ``` 8 | 9 | 10 | 11 | ## master 12 | 13 | Do not make direct modifications or push directly to this branch. When you need to fix bugs, create a new HOTFIX/* branch based on this branch for bug fixing. 14 | 15 | 16 | 17 | ## develop 18 | 19 | **Created based on the master branch.** 20 | 21 | If you already have a develop branch locally, before coding, pull and merge the latest develop branch. 22 | 23 | ```cmd 24 | Ensure you are on the develop branch. 25 | git checkout develop 26 | Pull updates from GitHub. 27 | git pull origin develop 28 | ``` 29 | 30 | If it does not exist, create one yourself: 31 | 32 | ```cmd 33 | git checkout -b develop 34 | ``` 35 | 36 | Note: Do not make direct modifications on this branch. When you need to add new features, create a new feature/* branch based on this develop branch for development. 37 | 38 | 39 | 40 | ## feature/* 41 | 42 | ***Represents any name that fits the context of your development or modification.** 43 | 44 | Created based on the develop branch. 45 | 46 | This branch is used for developing new features. 47 | 48 | For example, if you want to develop a getData function, create a feature/getData branch and start developing on this branch. 49 | 50 | ```cmd 51 | This command creates the feature/getData branch. 52 | git flow feature start getData 53 | 54 | ... 55 | 56 | Once your feature is complete and has undergone actual testing and unit tests, you can proceed to commit. 57 | pnpm commit 58 | Push your feature branch to GitHub and wait for my approval. Once approved, continue with the following steps. 59 | git push origin feature/getData 60 | (After approval) 61 | git flow feature finish getData // This operation will directly merge into the develop branch 62 | ``` 63 | 64 | 65 | 66 | ## hotfix/* 67 | 68 | ***Version number such as v3.0.2** 69 | 70 | Created based on the master branch. 71 | 72 | This branch is used for bug fixing. 73 | 74 | ```cmd 75 | git flow hotfix start v3.0.2 // This command creates the hotfix/v3.0.2 branch 76 | 77 | ... 78 | 79 | After fixing, 80 | pnpm commit 81 | Push this branch to GitHub (make sure to perform actual testing and unit tests beforehand). 82 | git push origin hotfix/v3.0.2 83 | 84 | Wait for my approval on your submission before proceeding. 85 | Merge back into master and develop branches, and tag the version. 86 | git flow hotfix finish v3.0.2 87 | git push origin v3.0.2 88 | ``` 89 | 90 | 91 | 92 | ## release/* 93 | 94 | ***Version number such as v3.0.0** 95 | 96 | Created based on the develop branch. 97 | 98 | This branch is used for releasing new versions. 99 | 100 | When the develop branch is completed and ready for a new version release, you can create a new release branch like v3.0.0. 101 | 102 | ```cmd 103 | git flow release start v3.0.0 // This command creates the release/v3.0.0 branch 104 | 105 | ... 106 | 107 | Upon completion, 108 | git push origin release/v3.0.0 109 | 110 | // Wait for approval before proceeding 111 | git flow release finish v3.0.0 112 | git push origin v3.0.0 113 | ``` 114 | 115 | 116 | 117 | ## chore/* 118 | 119 | ***Represents any name that fits the context of your development or modification.** 120 | 121 | Created based on the develop branch. 122 | 123 | This branch is used for adding documentation, correcting documentation, removing redundant dependencies, etc. 124 | 125 | ```cmd 126 | Create 127 | git checkout -b chore/* 128 | 129 | ... 130 | 131 | Upon completion of this branch: 132 | Commit the updated content (note to follow the prompts for writing content). 133 | pnpm commit 134 | Push the branch to GitHub for my review. If approved, continue with the following steps. 135 | git push origin chore/* 136 | 137 | After approval, continue by: 138 | git checkout develop 139 | First, pull to ensure you have the latest code. 140 | git pull origin develop 141 | git merge chore/* 142 | ``` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 这是一款基于axios开发的大文件分片上传插件支持断点续传以及暂停,开发者无需关注具体的实现逻辑即可快速实现 4 | 5 | 6 | 7 | ## 目录 8 | 9 | - [简介](#%E7%AE%80%E4%BB%8B) 10 | - [语言](#%E8%AF%AD%E8%A8%80) 11 | - [安装](#%E5%AE%89%E8%A3%85) 12 | - [示例](#%E7%A4%BA%E4%BE%8B) 13 | - [配置](#%E9%85%8D%E7%BD%AE) 14 | - [requestConfig](#requestconfig) 15 | - [uploadConfig](#uploadconfig) 16 | - [返回值](#%E8%BF%94%E5%9B%9E%E5%80%BC) 17 | - [完整示例代码](#%E5%AE%8C%E6%95%B4%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81) 18 | - [贡献指南](#%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97) 19 | 20 | 21 | 22 | 23 | ## 语言 24 | 25 | [English](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/README_en.md "English") 26 | [中文](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/README.md "中文") 27 | 28 | 29 | 30 | 31 | ## 安装 32 | 33 | ``` 34 | // npm 35 | npm i @ux-plus/chunk-uploader -S 36 | // yarn 37 | yarn add @ux-plus/chunk-uploader -S 38 | // pnpm 39 | pnpm add @ux-plus/chunk-uploader -S 40 | ``` 41 | 42 | 43 | 44 | ## 示例 45 | 46 | ```typescript 47 | import {type ERROR_RESPOSE,type Options,useUploader} from '@ux-plus/chunk-uploader'; 48 | const options:Options = { 49 | // 请求配置 50 | requestConfig: { 51 | // 获取已缓存分片的API配置 52 | getChunks: { 53 | url: '/api/chunks', 54 | /** 55 | * 拦截响应 56 | * 57 | * @param {AxiosResponse} r 58 | * @returns {{code:number;data:string[]}} 59 | */ 60 | onResponse(r) { 61 | return r.data; 62 | }, 63 | /** 64 | * 设置请求参数 65 | * @param {string} hash 66 | * @returns {Record} 67 | */ 68 | setParams(hash) { 69 | return { 70 | hash 71 | }; 72 | } 73 | }, 74 | // 用于上传分片的API配置 75 | uploadChunk: { 76 | url: '/api/uploadchunk', 77 | /** 78 | * 拦截响应 79 | * 80 | * @param {AxiosResponse} r 81 | * @returns {number} 82 | */ 83 | onResponse(r) { 84 | return r.data.code; 85 | }, 86 | /** 87 | * 设置请求参数 88 | * 89 | * @param {{ 90 | * dirname: string; 91 | * chunkname: string; 92 | * chunk: Blob; 93 | * }} r 94 | * @returns {Record} 95 | */ 96 | setParams(r) { 97 | return r; 98 | } 99 | }, 100 | // 用于合并分片的API配置 101 | merge: { 102 | url: '/api/mergechunks', 103 | /** 104 | * 拦截响应 105 | * 106 | * @param {AxiosResponse} r 107 | * @returns {number} 108 | */ 109 | onResponse(r) { 110 | return r.data.code; 111 | }, 112 | /** 113 | * 设置请求参数 114 | * 115 | * @param {{ hash: string; chunkcount: number; filename: string }} r 116 | * @returns {Record} 117 | */ 118 | setParams(r) { 119 | return r; 120 | } 121 | }, 122 | }, 123 | // 全局文件上传配置 124 | uploadConfig: { 125 | // 每个切片的大小,以MB为单位 126 | chunkSize: 100, 127 | // 上传的并发切片数 128 | limit: 5, 129 | // 最大切片数 130 | max: 100, 131 | }, 132 | }; 133 | const {onFail, onFinally, onSuccess, abort, trigger} = useUploader(options); 134 | 135 | 136 | trigger(/*your file*/,{ 137 | // 这里的配置会覆盖全局的上传文件配置 138 | { 139 | // 进度监听器 140 | onProgress(v) { 141 | console.log('onProgress', v); 142 | }, 143 | limit: 5, 144 | max: 100, 145 | }); 146 | 147 | // 上传和合并切片成功时调用 148 | onSuccess(()=>{ 149 | console.log("上传成功"); 150 | }); 151 | // 上传成功或合并切片失败时调用(包括取消上传) 152 | onFail((e:ERROR_RESPOSE)=>{ 153 | console.log("上传失败",e); 154 | }); 155 | // 上传以及合并切片请求完成后调用(包括取消上传) 156 | onFinally(()=>{ 157 | console.log("上传完成"); 158 | }); 159 | 160 | ``` 161 | 162 | 163 | 164 | ## 配置 165 | 166 | ### requestConfig 167 | 168 | - **getChunks**: 获取缓存分片的API配置。 169 | - `url`: 请求URL。 170 | - `onResponse(r)`: 响应拦截器,参数为Axios响应对象。 171 | - `setParams(hash)`: 设置请求参数,参数为文件哈希值。 172 | - **uploadChunk**: 分片上传API配置。 173 | - `url`: 请求URL。 174 | - `onResponse(r)`: 响应拦截器,参数为Axios响应对象。 175 | - `setParams(r)`: 设置请求参数,参数为包含`dirname`, `chunkname`, 和 `chunk`的对象。 176 | - **merge**: 合并分片API配置。 177 | - `url`: 请求URL。 178 | - `onResponse(r)`: 响应拦截器,参数为Axios响应对象。 179 | - `setParams(r)`: 设置请求参数,参数为包含`hash`, `chunkcount`, 和 `filename`的对象。 180 | 181 | ### uploadConfig 182 | 183 | - **chunkSize**: 每个分片大小(单位:MB)。 184 | - **limit**: 并发上传分片数量。 185 | - **max**: 最大分片数量。 186 | - **onProgress**:进度监听器。 187 | 188 | 189 | 190 | ## 返回值 191 | 192 | `useUploader` 返回一个对象,包含以下方法: 193 | 194 | - **onSuccess(callback)**: 文件上传及合并成功时调用的回调函数。 195 | 196 | - **onFail(callback)**: 文件上传或合并失败时调用的回调函数,包括上传取消。 197 | 198 | - **onFinally(callback)**: 文件上传及合并请求完成后调用的回调函数,无论成功与否。 199 | 200 | - **abort()**: 取消当前上传操作。 201 | 202 | - **trigger(file, config?)**: 触发文件上传,可覆盖全局配置项。 203 | 204 | 205 | 206 | ## 完整示例代码 207 | 208 | ```typescript 209 | import {type ERROR_RESPOSE,useUploader} from '@ux-plus/chunk-uploader'; 210 | const uploader = document.querySelector('#uploader')! as HTMLInputElement; 211 | const {onFail, onFinally, onSuccess, abort, trigger} = useUploader({ 212 | // 请求配置 213 | requestConfig: { 214 | // 获取以缓存分片接口配置 215 | getChunks: { 216 | url: '/api/chunks', 217 | /** 218 | * 拦截响应 219 | * 220 | * @param {AxiosResponse} r 221 | * @returns {{code:number;data:string[]}} 222 | */ 223 | onResponse(r) { 224 | return r.data; 225 | }, 226 | /** 227 | * 设置请求参数 228 | * @param {string} hash 229 | * @returns {Record} 230 | */ 231 | setParams(hash) { 232 | return { 233 | hash 234 | }; 235 | } 236 | }, 237 | // 用于上传切片的API配置 238 | uploadChunk: { 239 | url: '/api/uploadchunk', 240 | /** 241 | * 拦截响应 242 | * 243 | * @param {AxiosResponse} r 244 | * @returns {number} 245 | */ 246 | onResponse(r) { 247 | return r.data.code; 248 | }, 249 | /** 250 | * 设置请求参数 251 | * 252 | * @param {{ 253 | * dirname: string; 254 | * chunkname: string; 255 | * chunk: Blob; 256 | * }} r 257 | * @returns {Record} 258 | */ 259 | setParams(r) { 260 | return r; 261 | } 262 | }, 263 | // 用于合并分片的API配置 264 | merge: { 265 | url: '/api/mergechunks', 266 | /** 267 | * 拦截响应 268 | * 269 | * @param {AxiosResponse} r 270 | * @returns {number} 271 | */ 272 | onResponse(r) { 273 | return r.data.code; 274 | }, 275 | /** 276 | * 设置请求参数 277 | * 278 | * @param {{ hash: string; chunkcount: number; filename: string }} r 279 | * @returns {Record} 280 | */ 281 | setParams(r) { 282 | return r; 283 | } 284 | }, 285 | }, 286 | // 全局文件上传配置 287 | uploadConfig: { 288 | // 每个切片的大小,以MB为单位 289 | chunkSize: 100, 290 | // 上传的并发切片数 291 | limit: 5, 292 | // 最大分片数 293 | max: 100, 294 | }, 295 | }); 296 | 297 | const terminateBtn = document.querySelector('#terminate')! as HTMLInputElement; 298 | uploader.onchange = async (e: Event) => { 299 | const target = e.target as HTMLInputElement; 300 | const file = target.files![0]; 301 | trigger(file, 302 | // 这里的配置将覆盖全局文件上传配置。 303 | { 304 | /** 305 | * 上传进度监听器 306 | * 307 | * @param {number} v 308 | */ 309 | onProgress(v) { 310 | console.log('progress', v); 311 | }, 312 | limit: 5, 313 | max: 100, 314 | }); 315 | terminateBtn.onclick = () => { 316 | // 中断(取消)上传 317 | abort(); 318 | }; 319 | // 上传和切片合并成功时调用 320 | onSuccess(() => { 321 | console.log("上传成功了"); 322 | }); 323 | // 如果上传或分片合并失败(包括上传取消)调用。 324 | onFail((e: ERROR_RESPOSE) => { 325 | console.log("上传失败", e); 326 | }); 327 | // 在上传和切片合并请求完成时调用(包括上传取消) 328 | onFinally(() => { 329 | console.log("上传文件完毕"); 330 | }); 331 | }; 332 | ``` 333 | 334 | 335 | 336 | ## 贡献指南 337 | 338 | 感谢你对本项目感兴趣并愿意贡献!在开始之前,请仔细阅读以下指南。 339 | 340 | [详细文档](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/CONTRIBUTION.md) 341 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | ## introduction 2 | 3 | This is a large file chunked upload plugin based on axios development, supporting breakpoint resume and pause. Developers can quickly implement these features without concerning about the specific implementation logic. 4 | 5 | 6 | 7 | ## catalogue 8 | 9 | - [introduction](#introduction) 10 | - [Languge](#languge) 11 | - [Install](#install) 12 | - [Example](#example) 13 | - [Config](#config) 14 | - [requestConfig](#requestconfig) 15 | - [uploadConfig](#uploadconfig) 16 | - [Return Values](#return-values) 17 | - [Full example code](#full-example-code) 18 | - [Contribution Guideline](#contribution-guideline) 19 | 20 | 21 | 22 | ## Languge 23 | 24 | [English](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/README_en.md "English") 25 | [中文](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/README.md "中文") 26 | 27 | 28 | 29 | ## Install 30 | 31 | ```cmd 32 | // npm 33 | npm i @ux-plus/chunk-uploader -S 34 | // yarn 35 | yarn add @ux-plus/chunk-uploader -S 36 | // pnpm 37 | pnpm add @ux-plus/chunk-uploader -S 38 | ``` 39 | 40 | 41 | 42 | ## Example 43 | 44 | ```typescript 45 | import {type ERROR_RESPOSE,type Options,useUploader} from '@ux-plus/chunk-uploader'; 46 | const options:Options = { 47 | // Request configuration 48 | requestConfig: { 49 | // API for retrieving cached slices 50 | getChunks: { 51 | url: '/api/chunks', 52 | /** 53 | * Intercept Response 54 | * 55 | * @param {AxiosResponse} r 56 | * @returns {{code:number;data:string[]}} 57 | */ 58 | onResponse(r) { 59 | return r.data; 60 | }, 61 | /** 62 | * Set request parameters 63 | * @param {string} hash 64 | * @returns {Record} 65 | */ 66 | setParams(hash) { 67 | return { 68 | hash 69 | }; 70 | } 71 | }, 72 | // API for uploading slices 73 | uploadChunk: { 74 | url: '/api/uploadchunk', 75 | /** 76 | * Intercept Response 77 | * 78 | * @param {AxiosResponse} r 79 | * @returns {number} 80 | */ 81 | onResponse(r) { 82 | return r.data.code; 83 | }, 84 | /** 85 | * Set request parameters 86 | * 87 | * @param {{ 88 | * dirname: string; 89 | * chunkname: string; 90 | * chunk: Blob; 91 | * }} r 92 | * @returns {Record} 93 | */ 94 | setParams(r) { 95 | return r; 96 | } 97 | }, 98 | // API for merging slices 99 | merge: { 100 | url: '/api/mergechunks', 101 | /** 102 | * Intercept Response 103 | * 104 | * @param {AxiosResponse} r 105 | * @returns {number} 106 | */ 107 | onResponse(r) { 108 | return r.data.code; 109 | }, 110 | /** 111 | * Set request parameters 112 | * 113 | * @param {{ hash: string; chunkcount: number; filename: string }} r 114 | * @returns {Record} 115 | */ 116 | setParams(r) { 117 | return r; 118 | } 119 | }, 120 | }, 121 | // Global file upload configuration 122 | uploadConfig: { 123 | // Size of each slice in MB 124 | chunkSize: 100, 125 | // Number of concurrent slices to upload 126 | limit: 5, 127 | // Maximum number of slices 128 | max: 100, 129 | }, 130 | }; 131 | const {onFail, onFinally, onSuccess, abort, trigger} = useUploader(options); 132 | 133 | 134 | trigger(/* your file */, { 135 | // The configuration here will override the global file upload configuration 136 | onProgress(v) { 137 | console.log('onProgress', v); 138 | }, 139 | limit: 5, 140 | max: 100, 141 | }); 142 | 143 | // Called when upload and slice merging succeed 144 | onSuccess(() => { 145 | console.log("Upload succeeded"); 146 | }); 147 | 148 | // Called if the upload or slice merging fails (including upload cancellation) 149 | onFail((e: ERROR_RESPOSE) => { 150 | console.log("Upload failed", e); 151 | }); 152 | 153 | // Called once the upload and slice merging request completes (including upload cancellation) 154 | onFinally(() => { 155 | console.log("Upload completed"); 156 | }); 157 | ``` 158 | 159 | 160 | 161 | ## Config 162 | 163 | ### requestConfig 164 | 165 | - **getChunks**: Configuration for retrieving cached slices. 166 | - `url`: Request URL. 167 | - `onResponse(r)`: Response interceptor, parameter is Axios response object. 168 | - `setParams(hash)`: Set request parameters, parameter is file hash value. 169 | - **uploadChunk**: Configuration for uploading slices. 170 | - `url`: Request URL. 171 | - `onResponse(r)`: Response interceptor, parameter is Axios response object. 172 | - `setParams(r)`: Set request parameters, parameter includes `dirname`, `chunkname`, and `chunk`. 173 | - **merge**: Configuration for merging slices. 174 | - `url`: Request URL. 175 | - `onResponse(r)`: Response interceptor, parameter is Axios response object. 176 | - `setParams(r)`: Set request parameters, parameter includes `hash`, `chunkcount`, and `filename`. 177 | 178 | ### uploadConfig 179 | 180 | - **chunkSize**: Size of each slice in MB. 181 | - **limit**: Number of concurrent slices to upload. 182 | - **max**: Maximum number of slices. 183 | 184 | ## Return Values 185 | 186 | `useUploader` returns an object containing the following methods: 187 | 188 | - **onSuccess(callback)**: Called when the file upload and slice merging succeed. 189 | - **onFail(callback)**: Called if the file upload or slice merging fails (including upload cancellation). 190 | - **onFinally(callback)**: Called once the upload and slice merging request completes (including upload cancellation). 191 | - **abort()**: Cancel the current upload operation. 192 | - **trigger(file, config?)**: Trigger the file upload, which can override global configuration items. 193 | 194 | 195 | 196 | ## Full example code 197 | 198 | ```typescript 199 | import {type ERROR_RESPOSE,useUploader} from '@ux-plus/chunk-uploader'; 200 | const uploader = document.querySelector('#uploader')! as HTMLInputElement; 201 | const {onFail, onFinally, onSuccess, abort, trigger} = useUploader({ 202 | // Request configuration 203 | requestConfig: { 204 | // API for retrieving cached slices 205 | getChunks: { 206 | url: '/api/chunks', 207 | /** 208 | * Intercept Response 209 | * 210 | * @param {AxiosResponse} r 211 | * @returns {{code:number;data:string[]}} 212 | */ 213 | onResponse(r) { 214 | return r.data; 215 | }, 216 | /** 217 | * Set request parameters 218 | * @param {string} hash 219 | * @returns {Record} 220 | */ 221 | setParams(hash) { 222 | return { 223 | hash 224 | }; 225 | } 226 | }, 227 | // API for uploading slices 228 | uploadChunk: { 229 | url: '/api/uploadchunk', 230 | /** 231 | * Intercept Response 232 | * 233 | * @param {AxiosResponse} r 234 | * @returns {number} 235 | */ 236 | onResponse(r) { 237 | return r.data.code; 238 | }, 239 | /** 240 | * Set request parameters 241 | * 242 | * @param {{ 243 | * dirname: string; 244 | * chunkname: string; 245 | * chunk: Blob; 246 | * }} r 247 | * @returns {Record} 248 | */ 249 | setParams(r) { 250 | return r; 251 | } 252 | }, 253 | // API for merging slices 254 | merge: { 255 | url: '/api/mergechunks', 256 | /** 257 | * Intercept Response 258 | * 259 | * @param {AxiosResponse} r 260 | * @returns {number} 261 | */ 262 | onResponse(r) { 263 | return r.data.code; 264 | }, 265 | /** 266 | * Set request parameters 267 | * 268 | * @param {{ hash: string; chunkcount: number; filename: string }} r 269 | * @returns {Record} 270 | */ 271 | setParams(r) { 272 | return r; 273 | } 274 | }, 275 | }, 276 | // Global file upload configuration 277 | uploadConfig: { 278 | // Size of each slice in MB 279 | chunkSize: 100, 280 | // Number of concurrent slices to upload 281 | limit: 5, 282 | // Maximum number of slices 283 | max: 100, 284 | }, 285 | }); 286 | 287 | const terminateBtn = document.querySelector('#terminate')! as HTMLInputElement; 288 | uploader.onchange = async (e: Event) => { 289 | const target = e.target as HTMLInputElement; 290 | const file = target.files![0]; 291 | trigger(file, 292 | // The configuration here will override the global file upload configuration. 293 | { 294 | /** 295 | * Upload Progress Listener 296 | * 297 | * @param {number} v 298 | */ 299 | onProgress(v) { 300 | console.log('progress', v); 301 | }, 302 | limit: 5, 303 | max: 100, 304 | }); 305 | terminateBtn.onclick = () => { 306 | // Cancel upload 307 | abort(); 308 | }; 309 | // Called when upload and slice merging succeed 310 | onSuccess(() => { 311 | console.log("Upload succeeded"); 312 | }); 313 | // Called if the upload or slice merging fails (including upload cancellation) 314 | onFail((e: ERROR_RESPOSE) => { 315 | console.log("Upload failed", e); 316 | }); 317 | // Called once the upload and slice merging request completes (including upload cancellation) 318 | onFinally(() => { 319 | console.log("Upload completed"); 320 | }); 321 | }; 322 | ``` 323 | 324 | ## Contribution Guideline 325 | 326 | Thank you for your interest in this project and willingness to contribute! In the beginning, please read the following guidelines carefully. 327 | 328 | [Document](https://github.com/YTXEternal/ux-plus-chunk-uploader/blob/master/EXPLANATION_en.md) -------------------------------------------------------------------------------- /build/common.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | resolve: { 5 | alias: { 6 | "@": new URL("../src/packages", import.meta.url).pathname, 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /build/dev.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, defineConfig } from "vite"; 2 | import commonConfig from "./common"; 3 | 4 | export default mergeConfig( 5 | commonConfig, 6 | defineConfig({ 7 | server: { 8 | proxy: { 9 | "/api": { 10 | target: "http://localhost:3000", 11 | changeOrigin: true, 12 | rewrite: (path) => path.replace(/^\/api/, ""), 13 | }, 14 | }, 15 | }, 16 | }) 17 | ); 18 | -------------------------------------------------------------------------------- /build/lib.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, defineConfig } from "vite"; 2 | import { resolve } from "node:path"; 3 | import babel from "@rollup/plugin-babel"; 4 | import dts from "vite-plugin-dts"; 5 | import commonConfig from "./common"; 6 | 7 | const entry = resolve(__dirname, "../src/packages"); 8 | const outDir = resolve(__dirname, "../dist"); 9 | 10 | export default mergeConfig( 11 | commonConfig, 12 | defineConfig({ 13 | resolve: { 14 | alias: { 15 | "@": resolve(__dirname, "../src/packages"), 16 | }, 17 | }, 18 | build: { 19 | assetsDir: "ats", 20 | assetsInlineLimit: 0, 21 | outDir, 22 | lib: { 23 | entry, 24 | name: "index", 25 | fileName: "index", 26 | }, 27 | rollupOptions: { 28 | external: [/node:?.+/, "spark-md5", "axios", /\.d\.ts$/], 29 | output: [ 30 | { 31 | // ESM 32 | format: "es", 33 | //打包后文件名 34 | entryFileNames: "[name].js", 35 | //让打包目录和我们目录对应 36 | preserveModules: true, 37 | exports: "named", 38 | }, 39 | ], 40 | plugins: [ 41 | babel({ 42 | extensions: [".ts"], 43 | babelHelpers: "bundled", 44 | presets: [ 45 | [ 46 | "@babel/preset-env", 47 | { 48 | useBuiltIns: false, 49 | targets: { 50 | browsers: ["last 2 versions", "> 1%", "not ie <= 12"], 51 | }, 52 | }, 53 | ], 54 | ], 55 | }), 56 | ], 57 | }, 58 | }, 59 | plugins: [ 60 | dts({ 61 | rollupTypes: true, 62 | tsconfigPath: "./tsconfig.build.json", 63 | }), 64 | ], 65 | esbuild: { 66 | drop: ["console", "debugger"], 67 | }, 68 | worker: { 69 | format: "iife", 70 | }, 71 | }) 72 | ); 73 | -------------------------------------------------------------------------------- /build/test.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, defineConfig } from "vite"; 2 | import commonConfig from "./common"; 3 | 4 | export default mergeConfig(commonConfig, defineConfig({})); 5 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | const maxDepth = 3; 5 | const complexity = 20; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | { files: ["**/*.{js,mjs,cjs,ts}"] }, 10 | { 11 | ignores: [ 12 | "./server/**/*", 13 | "./dist/**/*", 14 | "**/node_modules/**", 15 | "**/*.{txt,md,json}", 16 | "examples/**/*", 17 | ], 18 | }, 19 | { languageOptions: { globals: globals.browser } }, 20 | pluginJs.configs.recommended, 21 | ...tseslint.configs.recommended, 22 | { 23 | rules: { 24 | "no-unused-vars": "off", 25 | complexity: ["error", complexity], 26 | "accessor-pairs": "error", 27 | "arrow-body-style": ["error", "as-needed"], 28 | "consistent-this": ["error", "context"], 29 | "default-case-last": "error", 30 | eqeqeq: "error", 31 | "func-names": ["error", "never"], 32 | "grouped-accessor-pairs": ["error", "getBeforeSet"], 33 | "guard-for-in": "error", 34 | "init-declarations": ["error", "always"], 35 | "max-depth": ["error", maxDepth], 36 | "no-alert": "error", 37 | "no-array-constructor": "error", 38 | "no-caller": "error", 39 | "no-case-declarations": "error", 40 | "no-delete-var": "error", 41 | "no-div-regex": "error", 42 | "no-else-return": "error", 43 | "no-empty": ["error", { allowEmptyCatch: true }], 44 | "no-empty-function": ["error", { allow: ["constructors"] }], 45 | "no-eval": "error", 46 | "no-eq-null": "error", 47 | "no-extend-native": "error", 48 | "no-extra-bind": "error", 49 | "no-extra-boolean-cast": "error", 50 | "no-extra-label": "error", 51 | "no-global-assign": "error", 52 | "no-implicit-coercion": "error", 53 | "no-implicit-globals": "error", 54 | "no-var": "error", 55 | "no-implied-eval": "error", 56 | "no-inline-comments": "error", 57 | "no-invalid-this": "error", 58 | "no-iterator": "error", 59 | "no-label-var": "error", 60 | "no-labels": "error", 61 | "no-lone-blocks": "error", 62 | "no-lonely-if": "error", 63 | "no-loop-func": "error", 64 | "no-multi-assign": "error", 65 | "no-multi-str": "error", 66 | "no-new-func": "error", 67 | "no-new-wrappers": "error", 68 | "no-nonoctal-decimal-escape": "error", 69 | "no-octal": "error", 70 | "no-octal-escape": "error", 71 | "no-param-reassign": ["error", { props: false }], 72 | "no-proto": "error", 73 | "no-redeclare": "error", 74 | "no-regex-spaces": "error", 75 | "no-return-assign": "error", 76 | "no-script-url": "error", 77 | "@typescript-eslint/no-explicit-any": "off", 78 | "no-shadow-restricted-names": "error", 79 | "no-throw-literal": "error", 80 | "no-undef-init": "error", 81 | "no-undefined": "error", 82 | "no-unused-labels": "error", 83 | "no-useless-call": "error", 84 | "no-useless-catch": "error", 85 | "no-useless-computed-key": "error", 86 | "no-useless-concat": "error", 87 | "no-useless-constructor": "error", 88 | "no-useless-escape": "error", 89 | "no-useless-return": "error", 90 | "no-with": "error", 91 | "object-shorthand": [ 92 | "error", 93 | "always", 94 | { avoidExplicitReturnArrows: true }, 95 | ], 96 | "prefer-rest-params": "error", 97 | "require-yield": "error", 98 | "symbol-description": "error", 99 | "no-await-in-loop": "error", 100 | "no-constructor-return": "error", 101 | "no-duplicate-imports": "error", 102 | "no-promise-executor-return": "off", 103 | "no-self-compare": "error", 104 | "no-template-curly-in-string": "error", 105 | "no-unreachable-loop": "error", 106 | "no-use-before-define": "error", 107 | "@typescript-eslint/ban-ts-comment": "off", 108 | "no-unsafe-function-type": "off", 109 | "no-unused-expressions": "off", 110 | "@typescript-eslint/no-unused-expressions": "off", 111 | "no-constant-binary-expression": "off", 112 | camelcase: "off", 113 | "no-nested-ternary": "off", 114 | "array-callback-return": "off", 115 | "no-underscore-dangle": "off", 116 | "@typescript-eslint/no-this-alias": "off", 117 | "@typescript-eslint/no-unused-vars": "error", 118 | }, 119 | }, 120 | ]; 121 | -------------------------------------------------------------------------------- /examples/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.customData": [".vscode/tailwindcss.json"] 3 | } -------------------------------------------------------------------------------- /examples/.vscode/tailwindcss.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@tailwind", 6 | "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", 7 | "references": [ 8 | { 9 | "name": "Tailwind Documentation", 10 | "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@apply", 16 | "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", 17 | "references": [ 18 | { 19 | "name": "Tailwind Documentation", 20 | "url": "https://tailwindcss.com/docs/functions-and-directives#apply" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "@responsive", 26 | "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", 27 | "references": [ 28 | { 29 | "name": "Tailwind Documentation", 30 | "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "@screen", 36 | "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", 37 | "references": [ 38 | { 39 | "name": "Tailwind Documentation", 40 | "url": "https://tailwindcss.com/docs/functions-and-directives#screen" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "@variants", 46 | "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", 47 | "references": [ 48 | { 49 | "name": "Tailwind Documentation", 50 | "url": "https://tailwindcss.com/docs/functions-and-directives#variants" 51 | } 52 | ] 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /examples/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const AButton: typeof import('antd/es')['Button'] 10 | const createRef: typeof import('react')['createRef'] 11 | const forwardRef: typeof import('react')['forwardRef'] 12 | const lazy: typeof import('react')['lazy'] 13 | const memo: typeof import('react')['memo'] 14 | const startTransition: typeof import('react')['startTransition'] 15 | const useCallback: typeof import('react')['useCallback'] 16 | const useContext: typeof import('react')['useContext'] 17 | const useDebugValue: typeof import('react')['useDebugValue'] 18 | const useDeferredValue: typeof import('react')['useDeferredValue'] 19 | const useEffect: typeof import('react')['useEffect'] 20 | const useId: typeof import('react')['useId'] 21 | const useImperativeHandle: typeof import('react')['useImperativeHandle'] 22 | const useInsertionEffect: typeof import('react')['useInsertionEffect'] 23 | const useLayoutEffect: typeof import('react')['useLayoutEffect'] 24 | const useMemo: typeof import('react')['useMemo'] 25 | const useReducer: typeof import('react')['useReducer'] 26 | const useRef: typeof import('react')['useRef'] 27 | const useState: typeof import('react')['useState'] 28 | const useSyncExternalStore: typeof import('react')['useSyncExternalStore'] 29 | const useTransition: typeof import('react')['useTransition'] 30 | } 31 | -------------------------------------------------------------------------------- /examples/build/resolvers/antd.ts: -------------------------------------------------------------------------------- 1 | export function kebabCase(key: string) { 2 | const result = key.replace(/([A-Z])/g, ' $1').trim(); 3 | return result.split(' ').join('-').toLowerCase(); 4 | } 5 | 6 | export type Awaitable = T | PromiseLike; 7 | 8 | export interface ImportInfo { 9 | as?: string; 10 | name?: string; 11 | from: string; 12 | } 13 | 14 | export type SideEffectsInfo = 15 | | (ImportInfo | string)[] 16 | | ImportInfo 17 | | string 18 | | undefined; 19 | 20 | export interface ComponentInfo extends ImportInfo { 21 | sideEffects?: SideEffectsInfo; 22 | } 23 | 24 | export type ComponentResolveResult = Awaitable< 25 | string | ComponentInfo | null | undefined | void 26 | >; 27 | 28 | export type ComponentResolverFunction = ( 29 | name: string 30 | ) => ComponentResolveResult; 31 | export interface ComponentResolverObject { 32 | type: 'component' | 'directive'; 33 | resolve: ComponentResolverFunction; 34 | } 35 | export type ComponentResolver = 36 | | ComponentResolverFunction 37 | | ComponentResolverObject; 38 | 39 | interface IMatcher { 40 | pattern: RegExp; 41 | styleDir: string; 42 | } 43 | 44 | const matchComponents: IMatcher[] = [ 45 | { 46 | pattern: /^Avatar/, 47 | styleDir: 'avatar', 48 | }, 49 | { 50 | pattern: /^AutoComplete/, 51 | styleDir: 'auto-complete', 52 | }, 53 | { 54 | pattern: /^Anchor/, 55 | styleDir: 'anchor', 56 | }, 57 | 58 | { 59 | pattern: /^Badge/, 60 | styleDir: 'badge', 61 | }, 62 | { 63 | pattern: /^Breadcrumb/, 64 | styleDir: 'breadcrumb', 65 | }, 66 | { 67 | pattern: /^Button/, 68 | styleDir: 'button', 69 | }, 70 | { 71 | pattern: /^Checkbox/, 72 | styleDir: 'checkbox', 73 | }, 74 | { 75 | pattern: /^Card/, 76 | styleDir: 'card', 77 | }, 78 | { 79 | pattern: /^Collapse/, 80 | styleDir: 'collapse', 81 | }, 82 | { 83 | pattern: /^Descriptions/, 84 | styleDir: 'descriptions', 85 | }, 86 | { 87 | pattern: /^RangePicker|^WeekPicker|^MonthPicker/, 88 | styleDir: 'date-picker', 89 | }, 90 | { 91 | pattern: /^Dropdown/, 92 | styleDir: 'dropdown', 93 | }, 94 | 95 | { 96 | pattern: /^Form/, 97 | styleDir: 'form', 98 | }, 99 | { 100 | pattern: /^InputNumber/, 101 | styleDir: 'input-number', 102 | }, 103 | 104 | { 105 | pattern: /^Input|^Textarea/, 106 | styleDir: 'input', 107 | }, 108 | { 109 | pattern: /^Statistic/, 110 | styleDir: 'statistic', 111 | }, 112 | { 113 | pattern: /^CheckableTag/, 114 | styleDir: 'tag', 115 | }, 116 | { 117 | pattern: /^TimeRangePicker/, 118 | styleDir: 'time-picker', 119 | }, 120 | { 121 | pattern: /^Layout/, 122 | styleDir: 'layout', 123 | }, 124 | { 125 | pattern: /^Menu|^SubMenu/, 126 | styleDir: 'menu', 127 | }, 128 | 129 | { 130 | pattern: /^Table/, 131 | styleDir: 'table', 132 | }, 133 | { 134 | pattern: /^TimePicker|^TimeRangePicker/, 135 | styleDir: 'time-picker', 136 | }, 137 | { 138 | pattern: /^Radio/, 139 | styleDir: 'radio', 140 | }, 141 | 142 | { 143 | pattern: /^Image/, 144 | styleDir: 'image', 145 | }, 146 | 147 | { 148 | pattern: /^List/, 149 | styleDir: 'list', 150 | }, 151 | 152 | { 153 | pattern: /^Tab/, 154 | styleDir: 'tabs', 155 | }, 156 | { 157 | pattern: /^Mentions/, 158 | styleDir: 'mentions', 159 | }, 160 | 161 | { 162 | pattern: /^Step/, 163 | styleDir: 'steps', 164 | }, 165 | { 166 | pattern: /^Skeleton/, 167 | styleDir: 'skeleton', 168 | }, 169 | 170 | { 171 | pattern: /^Select/, 172 | styleDir: 'select', 173 | }, 174 | { 175 | pattern: /^TreeSelect/, 176 | styleDir: 'tree-select', 177 | }, 178 | { 179 | pattern: /^Tree|^DirectoryTree/, 180 | styleDir: 'tree', 181 | }, 182 | { 183 | pattern: /^Typography/, 184 | styleDir: 'typography', 185 | }, 186 | { 187 | pattern: /^Timeline/, 188 | styleDir: 'timeline', 189 | }, 190 | { 191 | pattern: /^Upload/, 192 | styleDir: 'upload', 193 | }, 194 | ]; 195 | 196 | export interface AntDesignResolverOptions { 197 | /** 198 | * exclude components that do not require automatic import 199 | * 200 | * @default [] 201 | */ 202 | exclude?: string[]; 203 | /** 204 | * import style along with components 205 | * 206 | * @default 'css' 207 | */ 208 | importStyle?: boolean | 'css' | 'less'; 209 | /** 210 | * resolve `antd' icons 211 | * 212 | * requires package `@ant-design/icons-vue` 213 | * 214 | * @default false 215 | */ 216 | resolveIcons?: boolean; 217 | 218 | /** 219 | * @deprecated use `importStyle: 'css'` instead 220 | */ 221 | importCss?: boolean; 222 | /** 223 | * @deprecated use `importStyle: 'less'` instead 224 | */ 225 | importLess?: boolean; 226 | 227 | /** 228 | * use commonjs build default false 229 | */ 230 | cjs?: boolean; 231 | 232 | /** 233 | * rename package 234 | * 235 | * @default 'antd' 236 | */ 237 | packageName?: string; 238 | } 239 | 240 | function getStyleDir(compName: string): string { 241 | let styleDir; 242 | const total = matchComponents.length; 243 | for (let i = 0; i < total; i++) { 244 | const matcher = matchComponents[i]; 245 | if (compName.match(matcher.pattern)) { 246 | styleDir = matcher.styleDir; 247 | break; 248 | } 249 | } 250 | if (!styleDir) styleDir = kebabCase(compName); 251 | 252 | return styleDir; 253 | } 254 | 255 | function getSideEffects( 256 | compName: string, 257 | options: AntDesignResolverOptions 258 | ): SideEffectsInfo { 259 | const { importStyle = true, importLess = false } = options; 260 | 261 | if (!importStyle) return; 262 | const lib = options.cjs ? 'lib' : 'es'; 263 | const packageName = options?.packageName || 'antd'; 264 | 265 | if (importStyle === 'less' || importLess) { 266 | const styleDir = getStyleDir(compName); 267 | return `${packageName}/${lib}/${styleDir}/style`; 268 | } 269 | const styleDir = getStyleDir(compName); 270 | return `${packageName}/${lib}/${styleDir}/style`; 271 | 272 | } 273 | const primitiveNames = [ 274 | 'Affix', 275 | 'Anchor', 276 | 'AnchorLink', 277 | 'AutoComplete', 278 | 'AutoCompleteOptGroup', 279 | 'AutoCompleteOption', 280 | 'Alert', 281 | 'Avatar', 282 | 'AvatarGroup', 283 | 'BackTop', 284 | 'Badge', 285 | 'BadgeRibbon', 286 | 'Breadcrumb', 287 | 'BreadcrumbItem', 288 | 'BreadcrumbSeparator', 289 | 'Button', 290 | 'ButtonGroup', 291 | 'Calendar', 292 | 'Card', 293 | 'CardGrid', 294 | 'CardMeta', 295 | 'Collapse', 296 | 'CollapsePanel', 297 | 'Carousel', 298 | 'Cascader', 299 | 'Checkbox', 300 | 'CheckboxGroup', 301 | 'Col', 302 | 'Comment', 303 | 'ConfigProvider', 304 | 'DatePicker', 305 | 'MonthPicker', 306 | 'WeekPicker', 307 | 'RangePicker', 308 | 'QuarterPicker', 309 | 'Descriptions', 310 | 'DescriptionsItem', 311 | 'Divider', 312 | 'Dropdown', 313 | 'DropdownButton', 314 | 'Drawer', 315 | 'Empty', 316 | 'Form', 317 | 'FormItem', 318 | 'FormItemRest', 319 | 'Grid', 320 | 'Input', 321 | 'InputGroup', 322 | 'InputPassword', 323 | 'InputSearch', 324 | 'Textarea', 325 | 'Image', 326 | 'ImagePreviewGroup', 327 | 'InputNumber', 328 | 'Layout', 329 | 'LayoutHeader', 330 | 'LayoutSider', 331 | 'LayoutFooter', 332 | 'LayoutContent', 333 | 'List', 334 | 'ListItem', 335 | 'ListItemMeta', 336 | 'Menu', 337 | 'MenuDivider', 338 | 'MenuItem', 339 | 'MenuItemGroup', 340 | 'SubMenu', 341 | 'Mentions', 342 | 'MentionsOption', 343 | 'Modal', 344 | 'Statistic', 345 | 'StatisticCountdown', 346 | 'PageHeader', 347 | 'Pagination', 348 | 'Popconfirm', 349 | 'Popover', 350 | 'Progress', 351 | 'Radio', 352 | 'RadioButton', 353 | 'RadioGroup', 354 | 'Rate', 355 | 'Result', 356 | 'Row', 357 | 'Select', 358 | 'SelectOptGroup', 359 | 'SelectOption', 360 | 'Skeleton', 361 | 'SkeletonButton', 362 | 'SkeletonAvatar', 363 | 'SkeletonInput', 364 | 'SkeletonImage', 365 | 'Slider', 366 | 'Space', 367 | 'Spin', 368 | 'Steps', 369 | 'Step', 370 | 'Switch', 371 | 'Table', 372 | 'TableColumn', 373 | 'TableColumnGroup', 374 | 'TableSummary', 375 | 'TableSummaryRow', 376 | 'TableSummaryCell', 377 | 'Transfer', 378 | 'Tree', 379 | 'TreeNode', 380 | 'DirectoryTree', 381 | 'TreeSelect', 382 | 'TreeSelectNode', 383 | 'Tabs', 384 | 'TabPane', 385 | 'Tag', 386 | 'CheckableTag', 387 | 'TimePicker', 388 | 'TimeRangePicker', 389 | 'Timeline', 390 | 'TimelineItem', 391 | 'Tooltip', 392 | 'Typography', 393 | 'TypographyLink', 394 | 'TypographyParagraph', 395 | 'TypographyText', 396 | 'TypographyTitle', 397 | 'Upload', 398 | 'UploadDragger', 399 | 'LocaleProvider', 400 | ]; 401 | const prefix = 'A'; 402 | 403 | let antdNames: Set; 404 | 405 | function genAntdNames(primitiveNames: string[]): void { 406 | antdNames = new Set(primitiveNames.map((name) => `${prefix}${name}`)); 407 | } 408 | genAntdNames(primitiveNames); 409 | 410 | function isAntd(compName: string): boolean { 411 | return antdNames.has(compName); 412 | } 413 | 414 | 415 | export function AntDesignResolver( 416 | options: AntDesignResolverOptions = {} 417 | ): ComponentResolver { 418 | return { 419 | type: 'component', 420 | resolve(name: string) { 421 | if (options.resolveIcons && name.match(/(Outlined|Filled|TwoTone)$/)) { 422 | return { 423 | name, 424 | from: '@ant-design/icons', 425 | }; 426 | } 427 | 428 | if (isAntd(name) && !options?.exclude?.includes(name)) { 429 | const importName = name.slice(prefix.length); 430 | const { cjs = false, packageName = 'antd' } = options; 431 | const path = `${packageName}/${cjs ? 'lib' : 'es'}`; 432 | return { 433 | name: importName, 434 | from: path, 435 | sideEffects: getSideEffects(importName, options), 436 | }; 437 | } 438 | }, 439 | }; 440 | } 441 | 442 | -------------------------------------------------------------------------------- /examples/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite + React + TS 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons": "^6.0.0", 14 | "@ant-design/v5-patch-for-react-19": "^1.0.3", 15 | "@ux-plus/chunk-uploader": "link:C:/Users/25108/AppData/Local/pnpm/global/5/node_modules/@ux-plus/chunk-uploader", 16 | "antd": "^5.24.7", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.22.0", 22 | "@types/react": "^19.0.10", 23 | "@types/react-dom": "^19.0.4", 24 | "@unocss/preset-attributify": "65.5.0", 25 | "@unocss/preset-uno": "65.5.0", 26 | "@unocss/preset-web-fonts": "65.5.0", 27 | "@unocss/transformer-directives": "65.5.0", 28 | "@unocss/transformer-variant-group": "65.5.0", 29 | "@vitejs/plugin-react": "^4.3.4", 30 | "antd-dts": "^5.24.6", 31 | "eslint": "^9.22.0", 32 | "eslint-plugin-react-hooks": "^5.2.0", 33 | "eslint-plugin-react-refresh": "^0.4.19", 34 | "globals": "^16.0.0", 35 | "typescript": "~5.7.2", 36 | "typescript-eslint": "^8.26.1", 37 | "unocss": "65.5.0", 38 | "unplugin-auto-import": "^19.1.2", 39 | "vite": "^6.3.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | overrides: 2 | '@ux-plus/chunk-uploader': link:C:/Users/25108/AppData/Local/pnpm/global/5/node_modules/@ux-plus/chunk-uploader 3 | -------------------------------------------------------------------------------- /examples/src/App.css: -------------------------------------------------------------------------------- 1 | .box-center { 2 | @apply w-full h-full flex items-center justify-center 3 | } 4 | -------------------------------------------------------------------------------- /examples/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import Uploader from './components/Uploader'; 3 | import './plugins'; 4 | 5 | function App() { 6 | return ( 7 | <> 8 |
9 | 10 |
11 | 12 | ) 13 | } 14 | 15 | export default App 16 | -------------------------------------------------------------------------------- /examples/src/components/Uploader/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | 3 | import React,{useState} from 'react'; 4 | import { InboxOutlined } from '@ant-design/icons'; 5 | import { type UploadProps ,Upload,message } from 'antd'; 6 | import {type ERROR_RESPOSE,useUploader} from '@ux-plus/chunk-uploader'; 7 | 8 | const {trigger,onSuccess,onFail,abort,onFinally} = useUploader({ 9 | // 请求配置 10 | requestConfig: { 11 | // 获取已缓存切片接口s 12 | getChunks: { 13 | url: '/api/chunks', 14 | /** 15 | * @param {AxiosResponse} r 16 | * @returns {{ code: number; data: string[]; }} 17 | */ 18 | onResponse(r) { 19 | return r.data; 20 | }, 21 | setParams(hash) { 22 | return { 23 | hash 24 | } 25 | } 26 | }, 27 | // 上传切片接口 28 | uploadChunk: { 29 | url: '/api/uploadchunk', 30 | // 31 | onResponse(r) { 32 | return r.data.code; 33 | }, 34 | // 35 | setParams(r) { 36 | return r; 37 | } 38 | }, 39 | // 合并切片接口 40 | merge: { 41 | url: '/api/mergechunks', 42 | onResponse(r) { 43 | return r.data.code; 44 | }, 45 | setParams(r) { 46 | return r 47 | } 48 | }, 49 | }, 50 | // 上传配置 51 | uploadConfig: { 52 | // 每个切片大小(MB) 53 | chunkSize: 100, 54 | // 并发上传切片数 55 | limit: 5, 56 | // 最大分片数 57 | max: 100, 58 | } 59 | }); 60 | 61 | const { Dragger } = Upload; 62 | 63 | const App: React.FC = () => { 64 | const [isDisabled] = useState(false); 65 | onFinally(()=>{ 66 | console.log('上传完毕'); 67 | }); 68 | const props: UploadProps = { 69 | name: 'file', 70 | multiple: false, 71 | beforeUpload() { 72 | abort(); 73 | return true; 74 | }, 75 | onRemove() { 76 | abort(); 77 | }, 78 | customRequest(params) { 79 | const raw = params.file; 80 | onSuccess(()=>{ 81 | message.success('文件上传成功'); 82 | params.onSuccess!(void 0); 83 | }); 84 | onFail((e:ERROR_RESPOSE)=>{ 85 | // console.error(e); 86 | message.error(e.reason); 87 | params.onError!(new Error('上传失败')); 88 | }); 89 | trigger(raw as File,{ 90 | onProgress(r) { 91 | params.onProgress!({ 92 | percent:r 93 | }); 94 | }, 95 | limit:4, 96 | 'max':100, 97 | 'chunkSize':50 98 | }) 99 | .catch(()=>{ 100 | console.log('失败'); 101 | }); 102 | }, 103 | maxCount:1, 104 | disabled:isDisabled, 105 | }; 106 | 107 | return ( 108 | 109 |

110 | 111 |

112 |

大文件上传

113 |

114 | 仅支持单个上传 115 |

116 |
117 | ) 118 | }; 119 | 120 | export default App; -------------------------------------------------------------------------------- /examples/src/index.css: -------------------------------------------------------------------------------- 1 | body,html,#root { 2 | @apply h-full w-full 3 | } -------------------------------------------------------------------------------- /examples/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | import '@ant-design/v5-patch-for-react-19'; 6 | import './styles/index.css'; 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /examples/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import './unocss'; -------------------------------------------------------------------------------- /examples/src/plugins/unocss.ts: -------------------------------------------------------------------------------- 1 | import 'virtual:uno.css' -------------------------------------------------------------------------------- /examples/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import './normalize.css'; -------------------------------------------------------------------------------- /examples/src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /examples/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "types":["antd-dts"] 25 | }, 26 | "include": ["src","auto-imports.d.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | 8 | } 9 | -------------------------------------------------------------------------------- /examples/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/unocss.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'unocss/vite' 2 | import transformerVariantGroup from '@unocss/transformer-variant-group'; 3 | import presetUno from '@unocss/preset-uno'; 4 | import presetAttributify from '@unocss/preset-attributify'; 5 | import presetWebFonts from '@unocss/preset-web-fonts'; 6 | import transformerDirectives from '@unocss/transformer-directives'; 7 | export default defineConfig({ 8 | transformers: [transformerVariantGroup(),transformerDirectives({})], 9 | presets:[presetUno(),presetAttributify(),presetWebFonts()] 10 | }) -------------------------------------------------------------------------------- /examples/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { AntDesignResolver } from './build/resolvers/antd'; 4 | import AutoImport from 'unplugin-auto-import/vite'; 5 | import UnoCSS from 'unocss/vite' 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | react(), 11 | AutoImport({ 12 | imports: ['react'], 13 | dts: true, 14 | resolvers: [ 15 | AntDesignResolver({ 16 | resolveIcons: true, 17 | }), 18 | ], 19 | }), 20 | UnoCSS({ 21 | 'configFile':'./unocss.config.ts' 22 | }) 23 | ], 24 | server:{ 25 | proxy:{ 26 | '/api':{ 27 | target:'http://localhost:3000', 28 | changeOrigin: true, 29 | rewrite: (path) => path.replace(/^\/api/, ''), 30 | } 31 | } 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uploader 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ux-plus/chunk-uploader", 3 | "public": true, 4 | "version": "2.0.7", 5 | "description": "这是一款基于axios开发的大文件分片上传插件支持断点续传以及暂停,开发者无需关注具体的实现逻辑即可快速实现", 6 | "type": "module", 7 | "main": "./dist/index.js", 8 | "module": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "dev": "npx vite dev --config ./build/dev.ts", 12 | "build": "npx vite build --config ./build/lib.ts", 13 | "lint": "npx eslint --fix", 14 | "test": "npx vitest", 15 | "lint-staged": "lint-staged", 16 | "husky": "pnpm exec husky init", 17 | "commit": "pnpm lint && git add . && npx cz", 18 | "coverage": "vitest run --coverage", 19 | "testui": "npx vitest --ui" 20 | }, 21 | "exports": { 22 | ".": { 23 | "module-sync": "./dist/index.js", 24 | "import": "./dist/index.js", 25 | "types": "./dist/index.d.ts" 26 | } 27 | }, 28 | "author": "ux_rcl", 29 | "files": [ 30 | "dist" 31 | ], 32 | "license": "Apache-2.0", 33 | "licenses": [ 34 | { 35 | "type": "Apache-2.0", 36 | "url": "https://opensource.org/licenses/apache2.0.php" 37 | } 38 | ], 39 | "bugs": { 40 | "url": "https://github.com/YTXEternal/ux-plus-chunk-uploader/issues" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/YTXEternal/ux-plus-chunk-uploader" 45 | }, 46 | "keywords": [ 47 | "file upload", 48 | "ux-plus" 49 | ], 50 | "publishConfig": { 51 | "access": "public", 52 | "registry": "https://registry.npmjs.org" 53 | }, 54 | "readme": "README.md", 55 | "devDependencies": { 56 | "@babel/preset-env": "^7.26.9", 57 | "@commitlint/cli": "^19.8.0", 58 | "@commitlint/config-conventional": "^19.8.0", 59 | "@eslint/js": "^9.24.0", 60 | "@rollup/plugin-babel": "^6.0.4", 61 | "@types/node": "^22.14.1", 62 | "@types/spark-md5": "^3.0.5", 63 | "@typescript-eslint/eslint-plugin": "^8.29.1", 64 | "@typescript-eslint/parser": "^8.29.1", 65 | "@vitest/coverage-v8": "^3.1.1", 66 | "@vitest/ui": "^3.1.1", 67 | "axios-mock-adapter": "^2.1.0", 68 | "commitizen": "^4.3.1", 69 | "cz-conventional-changelog": "^3.3.0", 70 | "eslint": "^9.24.0", 71 | "globals": "^16.0.0", 72 | "happy-dom": "^17.4.4", 73 | "husky": "^9.1.7", 74 | "jsdom": "^26.1.0", 75 | "lint-staged": "^15.5.1", 76 | "tsconfig-paths": "^4.2.0", 77 | "typescript": "~5.8.3", 78 | "typescript-eslint": "^8.29.1", 79 | "vite": "^6.2.6", 80 | "vite-plugin-dts": "^4.5.3", 81 | "vite-tsconfig-paths": "^5.1.4", 82 | "vitest": "^3.1.1" 83 | }, 84 | "config": { 85 | "commitizen": { 86 | "path": "./node_modules/cz-conventional-changelog" 87 | } 88 | }, 89 | "dependencies": { 90 | "axios": "^1.8.4", 91 | "spark-md5": "^3.0.2" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var createError = require('http-errors'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | var cookieParser = require('cookie-parser'); 5 | var logger = require('morgan'); 6 | const cors = require('cors'); 7 | var indexRouter = require('./routes/index'); 8 | 9 | var app = express(); 10 | app.use(cors()); 11 | app.set('views', path.join(__dirname, 'views')); 12 | app.set('view engine', 'pug'); 13 | 14 | app.use(logger('dev')); 15 | app.use(express.json()); 16 | app.use(express.urlencoded({ extended: false })); 17 | app.use(cookieParser()); 18 | app.use(express.static(path.join(__dirname, 'public'))); 19 | 20 | app.use(indexRouter); 21 | 22 | // catch 404 and forward to error handler 23 | app.use(function(req, res, next) { 24 | next(createError(404)); 25 | }); 26 | 27 | // error handler 28 | app.use(function(err, req, res, next) { 29 | // set locals, only providing error in development 30 | res.locals.message = err.message; 31 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 32 | 33 | // render the error page 34 | res.status(err.status || 500); 35 | res.render('error'); 36 | }); 37 | 38 | module.exports = app; 39 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require("../app"); 8 | var debug = require("debug")("server:server"); 9 | var http = require("http"); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || "3000"); 16 | app.set("port", port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on("error", onError); 30 | server.on("listening", onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== "listen") { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; 62 | 63 | // handle specific listen errors with friendly messages 64 | switch (error.code) { 65 | case "EACCES": 66 | console.error(bind + " requires elevated privileges"); 67 | process.exit(1); 68 | case "EADDRINUSE": 69 | console.error(bind + " is already in use"); 70 | default: 71 | throw error; 72 | } 73 | } 74 | 75 | /** 76 | * Event listener for HTTP server "listening" event. 77 | */ 78 | 79 | function onListening() { 80 | var addr = server.address(); 81 | var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; 82 | debug("Listening on " + bind); 83 | } 84 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nodemon ./bin/www" 7 | }, 8 | "dependencies": { 9 | "cookie-parser": "~1.4.4", 10 | "cors": "^2.8.5", 11 | "debug": "~2.6.9", 12 | "express": "~4.16.1", 13 | "fs-extra": "^11.2.0", 14 | "http-errors": "~1.6.3", 15 | "morgan": "~1.9.1", 16 | "multer": "^1.4.5-lts.1", 17 | "pug": "2.0.0-beta11" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | cookie-parser: 12 | specifier: ~1.4.4 13 | version: 1.4.7 14 | cors: 15 | specifier: ^2.8.5 16 | version: 2.8.5 17 | debug: 18 | specifier: ~2.6.9 19 | version: 2.6.9 20 | express: 21 | specifier: ~4.16.1 22 | version: 4.16.4 23 | fs-extra: 24 | specifier: ^11.2.0 25 | version: 11.2.0 26 | http-errors: 27 | specifier: ~1.6.3 28 | version: 1.6.3 29 | morgan: 30 | specifier: ~1.9.1 31 | version: 1.9.1 32 | multer: 33 | specifier: ^1.4.5-lts.1 34 | version: 1.4.5-lts.1 35 | pug: 36 | specifier: 2.0.0-beta11 37 | version: 2.0.0-beta11 38 | 39 | packages: 40 | 41 | '@types/babel-types@7.0.16': 42 | resolution: {integrity: sha512-5QXs9GBFTNTmilLlWBhnsprqpjfrotyrnzUdwDrywEL/DA4LuCWQT300BTOXA3Y9ngT9F2uvmCoIxI6z8DlJEA==} 43 | 44 | '@types/babylon@6.16.9': 45 | resolution: {integrity: sha512-sEKyxMVEowhcr8WLfN0jJYe4gS4Z9KC2DGz0vqfC7+MXFbmvOF7jSjALC77thvAO2TLgFUPa9vDeOak+AcUrZA==} 46 | 47 | accepts@1.3.8: 48 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 49 | engines: {node: '>= 0.6'} 50 | 51 | acorn-globals@3.1.0: 52 | resolution: {integrity: sha512-uWttZCk96+7itPxK8xCzY86PnxKTMrReKDqrHzv42VQY0K30PUO8WY13WMOuI+cOdX4EIdzdvQ8k6jkuGRFMYw==} 53 | 54 | acorn@3.3.0: 55 | resolution: {integrity: sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==} 56 | engines: {node: '>=0.4.0'} 57 | hasBin: true 58 | 59 | acorn@4.0.13: 60 | resolution: {integrity: sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==} 61 | engines: {node: '>=0.4.0'} 62 | hasBin: true 63 | 64 | align-text@0.1.4: 65 | resolution: {integrity: sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==} 66 | engines: {node: '>=0.10.0'} 67 | 68 | amdefine@1.0.1: 69 | resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} 70 | engines: {node: '>=0.4.2'} 71 | 72 | append-field@1.0.0: 73 | resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} 74 | 75 | array-flatten@1.1.1: 76 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 77 | 78 | asap@2.0.6: 79 | resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} 80 | 81 | babel-runtime@6.26.0: 82 | resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} 83 | 84 | babel-types@6.26.0: 85 | resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} 86 | 87 | babylon@6.18.0: 88 | resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} 89 | hasBin: true 90 | 91 | basic-auth@2.0.1: 92 | resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} 93 | engines: {node: '>= 0.8'} 94 | 95 | body-parser@1.18.3: 96 | resolution: {integrity: sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==} 97 | engines: {node: '>= 0.8'} 98 | 99 | buffer-from@1.1.2: 100 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 101 | 102 | busboy@1.6.0: 103 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 104 | engines: {node: '>=10.16.0'} 105 | 106 | bytes@3.0.0: 107 | resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} 108 | engines: {node: '>= 0.8'} 109 | 110 | call-bind@1.0.7: 111 | resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} 112 | engines: {node: '>= 0.4'} 113 | 114 | camelcase@1.2.1: 115 | resolution: {integrity: sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==} 116 | engines: {node: '>=0.10.0'} 117 | 118 | center-align@0.1.3: 119 | resolution: {integrity: sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==} 120 | engines: {node: '>=0.10.0'} 121 | 122 | character-parser@2.2.0: 123 | resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} 124 | 125 | clean-css@3.4.28: 126 | resolution: {integrity: sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==} 127 | engines: {node: '>=0.10.0'} 128 | hasBin: true 129 | 130 | cliui@2.1.0: 131 | resolution: {integrity: sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==} 132 | 133 | commander@2.8.1: 134 | resolution: {integrity: sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==} 135 | engines: {node: '>= 0.6.x'} 136 | 137 | concat-stream@1.6.2: 138 | resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} 139 | engines: {'0': node >= 0.8} 140 | 141 | constantinople@3.1.2: 142 | resolution: {integrity: sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==} 143 | 144 | content-disposition@0.5.2: 145 | resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} 146 | engines: {node: '>= 0.6'} 147 | 148 | content-type@1.0.5: 149 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 150 | engines: {node: '>= 0.6'} 151 | 152 | cookie-parser@1.4.7: 153 | resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} 154 | engines: {node: '>= 0.8.0'} 155 | 156 | cookie-signature@1.0.6: 157 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 158 | 159 | cookie@0.3.1: 160 | resolution: {integrity: sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==} 161 | engines: {node: '>= 0.6'} 162 | 163 | cookie@0.7.2: 164 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 165 | engines: {node: '>= 0.6'} 166 | 167 | core-js@2.6.12: 168 | resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} 169 | deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. 170 | 171 | core-util-is@1.0.3: 172 | resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 173 | 174 | cors@2.8.5: 175 | resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 176 | engines: {node: '>= 0.10'} 177 | 178 | debug@2.6.9: 179 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 180 | peerDependencies: 181 | supports-color: '*' 182 | peerDependenciesMeta: 183 | supports-color: 184 | optional: true 185 | 186 | decamelize@1.2.0: 187 | resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} 188 | engines: {node: '>=0.10.0'} 189 | 190 | define-data-property@1.1.4: 191 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 192 | engines: {node: '>= 0.4'} 193 | 194 | depd@1.1.2: 195 | resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} 196 | engines: {node: '>= 0.6'} 197 | 198 | destroy@1.0.4: 199 | resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} 200 | 201 | doctypes@1.1.0: 202 | resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} 203 | 204 | ee-first@1.1.1: 205 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 206 | 207 | encodeurl@1.0.2: 208 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 209 | engines: {node: '>= 0.8'} 210 | 211 | es-define-property@1.0.0: 212 | resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} 213 | engines: {node: '>= 0.4'} 214 | 215 | es-errors@1.3.0: 216 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 217 | engines: {node: '>= 0.4'} 218 | 219 | escape-html@1.0.3: 220 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 221 | 222 | esutils@2.0.3: 223 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 224 | engines: {node: '>=0.10.0'} 225 | 226 | etag@1.8.1: 227 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 228 | engines: {node: '>= 0.6'} 229 | 230 | express@4.16.4: 231 | resolution: {integrity: sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==} 232 | engines: {node: '>= 0.10.0'} 233 | 234 | finalhandler@1.1.1: 235 | resolution: {integrity: sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==} 236 | engines: {node: '>= 0.8'} 237 | 238 | forwarded@0.2.0: 239 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 240 | engines: {node: '>= 0.6'} 241 | 242 | fresh@0.5.2: 243 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 244 | engines: {node: '>= 0.6'} 245 | 246 | fs-extra@11.2.0: 247 | resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} 248 | engines: {node: '>=14.14'} 249 | 250 | function-bind@1.1.2: 251 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 252 | 253 | get-intrinsic@1.2.4: 254 | resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} 255 | engines: {node: '>= 0.4'} 256 | 257 | gopd@1.0.1: 258 | resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 259 | 260 | graceful-fs@4.2.11: 261 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 262 | 263 | graceful-readlink@1.0.1: 264 | resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} 265 | 266 | has-property-descriptors@1.0.2: 267 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 268 | 269 | has-proto@1.0.3: 270 | resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} 271 | engines: {node: '>= 0.4'} 272 | 273 | has-symbols@1.0.3: 274 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 275 | engines: {node: '>= 0.4'} 276 | 277 | has-tostringtag@1.0.2: 278 | resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 279 | engines: {node: '>= 0.4'} 280 | 281 | hasown@2.0.2: 282 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 283 | engines: {node: '>= 0.4'} 284 | 285 | http-errors@1.6.3: 286 | resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} 287 | engines: {node: '>= 0.6'} 288 | 289 | iconv-lite@0.4.23: 290 | resolution: {integrity: sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==} 291 | engines: {node: '>=0.10.0'} 292 | 293 | inherits@2.0.3: 294 | resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} 295 | 296 | inherits@2.0.4: 297 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 298 | 299 | ipaddr.js@1.9.1: 300 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 301 | engines: {node: '>= 0.10'} 302 | 303 | is-buffer@1.1.6: 304 | resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} 305 | 306 | is-core-module@2.15.1: 307 | resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} 308 | engines: {node: '>= 0.4'} 309 | 310 | is-expression@3.0.0: 311 | resolution: {integrity: sha512-vyMeQMq+AiH5uUnoBfMTwf18tO3bM6k1QXBE9D6ueAAquEfCZe3AJPtud9g6qS0+4X8xA7ndpZiDyeb2l2qOBw==} 312 | 313 | is-promise@2.2.2: 314 | resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} 315 | 316 | is-regex@1.1.4: 317 | resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} 318 | engines: {node: '>= 0.4'} 319 | 320 | isarray@1.0.0: 321 | resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} 322 | 323 | js-stringify@1.0.2: 324 | resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} 325 | 326 | jsonfile@6.1.0: 327 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 328 | 329 | jstransformer@1.0.0: 330 | resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} 331 | 332 | kind-of@3.2.2: 333 | resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} 334 | engines: {node: '>=0.10.0'} 335 | 336 | lazy-cache@1.0.4: 337 | resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} 338 | engines: {node: '>=0.10.0'} 339 | 340 | lodash@4.17.21: 341 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 342 | 343 | longest@1.0.1: 344 | resolution: {integrity: sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==} 345 | engines: {node: '>=0.10.0'} 346 | 347 | media-typer@0.3.0: 348 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 349 | engines: {node: '>= 0.6'} 350 | 351 | merge-descriptors@1.0.1: 352 | resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} 353 | 354 | methods@1.1.2: 355 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 356 | engines: {node: '>= 0.6'} 357 | 358 | mime-db@1.52.0: 359 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 360 | engines: {node: '>= 0.6'} 361 | 362 | mime-types@2.1.35: 363 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 364 | engines: {node: '>= 0.6'} 365 | 366 | mime@1.4.1: 367 | resolution: {integrity: sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==} 368 | hasBin: true 369 | 370 | minimist@1.2.8: 371 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 372 | 373 | mkdirp@0.5.6: 374 | resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} 375 | hasBin: true 376 | 377 | morgan@1.9.1: 378 | resolution: {integrity: sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==} 379 | engines: {node: '>= 0.8.0'} 380 | 381 | ms@2.0.0: 382 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 383 | 384 | multer@1.4.5-lts.1: 385 | resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} 386 | engines: {node: '>= 6.0.0'} 387 | 388 | negotiator@0.6.3: 389 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 390 | engines: {node: '>= 0.6'} 391 | 392 | object-assign@4.1.1: 393 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 394 | engines: {node: '>=0.10.0'} 395 | 396 | on-finished@2.3.0: 397 | resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} 398 | engines: {node: '>= 0.8'} 399 | 400 | on-headers@1.0.2: 401 | resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} 402 | engines: {node: '>= 0.8'} 403 | 404 | parseurl@1.3.3: 405 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 406 | engines: {node: '>= 0.8'} 407 | 408 | path-parse@1.0.7: 409 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 410 | 411 | path-to-regexp@0.1.7: 412 | resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} 413 | 414 | process-nextick-args@2.0.1: 415 | resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} 416 | 417 | promise@7.3.1: 418 | resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} 419 | 420 | proxy-addr@2.0.7: 421 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 422 | engines: {node: '>= 0.10'} 423 | 424 | pug-attrs@2.0.4: 425 | resolution: {integrity: sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==} 426 | 427 | pug-code-gen@1.1.1: 428 | resolution: {integrity: sha512-UwZaJVhjhy2kYntLqXjSV1ae+K96ve6bG+N5bLFfA6yyGJTEkguct19MWDyUM9D8CDU3NNxVctUAh5McF19E6w==} 429 | 430 | pug-error@1.3.3: 431 | resolution: {integrity: sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==} 432 | 433 | pug-filters@2.1.5: 434 | resolution: {integrity: sha512-xkw71KtrC4sxleKiq+cUlQzsiLn8pM5+vCgkChW2E6oNOzaqTSIBKIQ5cl4oheuDzvJYCTSYzRaVinMUrV4YLQ==} 435 | 436 | pug-lexer@3.1.0: 437 | resolution: {integrity: sha512-DxXOrmCIDVEwzN2ozZBK1t4QRTR6pLv5YkqM6dLdaSHnm+LJJRBngVn4IDMMBZQR9xUpxrRm9rffmku2OEqkJw==} 438 | 439 | pug-linker@2.0.3: 440 | resolution: {integrity: sha512-ZqKljvFUl1K5L4G5WABJ5FUYWOY0K2AXLmwj2QfM7nPCUcxfsmr05SikjgXGXVoIrygGzM/iWSsXwnkWId4AHw==} 441 | 442 | pug-load@2.0.12: 443 | resolution: {integrity: sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==} 444 | 445 | pug-parser@2.0.2: 446 | resolution: {integrity: sha512-PW8kKDLN07MbFljR/GaYHPBGW+64YldtFFZUEGltJ67RRzebI/DxZy4njlxacy9JeheosyVprZ9C5DIexG1D/Q==} 447 | 448 | pug-runtime@2.0.5: 449 | resolution: {integrity: sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==} 450 | 451 | pug-strip-comments@1.0.4: 452 | resolution: {integrity: sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==} 453 | 454 | pug-walk@1.1.8: 455 | resolution: {integrity: sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==} 456 | 457 | pug@2.0.0-beta11: 458 | resolution: {integrity: sha512-iV0ibDCWLJGw8eEtBKAqbJZecOabQa6hpFeH+GCBzsAsCNSvpjo4wuHMPcmqtaZhxoO3ElbMePf8jkrM9TKulw==} 459 | 460 | qs@6.5.2: 461 | resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==} 462 | engines: {node: '>=0.6'} 463 | 464 | range-parser@1.2.1: 465 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 466 | engines: {node: '>= 0.6'} 467 | 468 | raw-body@2.3.3: 469 | resolution: {integrity: sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==} 470 | engines: {node: '>= 0.8'} 471 | 472 | readable-stream@2.3.8: 473 | resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} 474 | 475 | regenerator-runtime@0.11.1: 476 | resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} 477 | 478 | repeat-string@1.6.1: 479 | resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} 480 | engines: {node: '>=0.10'} 481 | 482 | resolve@1.22.8: 483 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 484 | hasBin: true 485 | 486 | right-align@0.1.3: 487 | resolution: {integrity: sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==} 488 | engines: {node: '>=0.10.0'} 489 | 490 | safe-buffer@5.1.2: 491 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 492 | 493 | safer-buffer@2.1.2: 494 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 495 | 496 | send@0.16.2: 497 | resolution: {integrity: sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==} 498 | engines: {node: '>= 0.8.0'} 499 | 500 | serve-static@1.13.2: 501 | resolution: {integrity: sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==} 502 | engines: {node: '>= 0.8.0'} 503 | 504 | set-function-length@1.2.2: 505 | resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} 506 | engines: {node: '>= 0.4'} 507 | 508 | setprototypeof@1.1.0: 509 | resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} 510 | 511 | source-map@0.4.4: 512 | resolution: {integrity: sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==} 513 | engines: {node: '>=0.8.0'} 514 | 515 | source-map@0.5.7: 516 | resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} 517 | engines: {node: '>=0.10.0'} 518 | 519 | statuses@1.4.0: 520 | resolution: {integrity: sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==} 521 | engines: {node: '>= 0.6'} 522 | 523 | statuses@1.5.0: 524 | resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} 525 | engines: {node: '>= 0.6'} 526 | 527 | streamsearch@1.1.0: 528 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 529 | engines: {node: '>=10.0.0'} 530 | 531 | string_decoder@1.1.1: 532 | resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} 533 | 534 | supports-preserve-symlinks-flag@1.0.0: 535 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 536 | engines: {node: '>= 0.4'} 537 | 538 | to-fast-properties@1.0.3: 539 | resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} 540 | engines: {node: '>=0.10.0'} 541 | 542 | token-stream@0.0.1: 543 | resolution: {integrity: sha512-nfjOAu/zAWmX9tgwi5NRp7O7zTDUD1miHiB40klUnAh9qnL1iXdgzcz/i5dMaL5jahcBAaSfmNOBBJBLJW8TEg==} 544 | 545 | type-is@1.6.18: 546 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 547 | engines: {node: '>= 0.6'} 548 | 549 | typedarray@0.0.6: 550 | resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} 551 | 552 | uglify-js@2.8.29: 553 | resolution: {integrity: sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==} 554 | engines: {node: '>=0.8.0'} 555 | hasBin: true 556 | 557 | uglify-to-browserify@1.0.2: 558 | resolution: {integrity: sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==} 559 | 560 | universalify@2.0.1: 561 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} 562 | engines: {node: '>= 10.0.0'} 563 | 564 | unpipe@1.0.0: 565 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 566 | engines: {node: '>= 0.8'} 567 | 568 | util-deprecate@1.0.2: 569 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 570 | 571 | utils-merge@1.0.1: 572 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 573 | engines: {node: '>= 0.4.0'} 574 | 575 | vary@1.1.2: 576 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 577 | engines: {node: '>= 0.8'} 578 | 579 | void-elements@2.0.1: 580 | resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==} 581 | engines: {node: '>=0.10.0'} 582 | 583 | window-size@0.1.0: 584 | resolution: {integrity: sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==} 585 | engines: {node: '>= 0.8.0'} 586 | 587 | with@5.1.1: 588 | resolution: {integrity: sha512-uAnSsFGfSpF6DNhBXStvlZILfHJfJu4eUkfbRGk94kGO1Ta7bg6FwfvoOhhyHAJuFbCw+0xk4uJ3u57jLvlCJg==} 589 | 590 | wordwrap@0.0.2: 591 | resolution: {integrity: sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==} 592 | engines: {node: '>=0.4.0'} 593 | 594 | xtend@4.0.2: 595 | resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 596 | engines: {node: '>=0.4'} 597 | 598 | yargs@3.10.0: 599 | resolution: {integrity: sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==} 600 | 601 | snapshots: 602 | 603 | '@types/babel-types@7.0.16': {} 604 | 605 | '@types/babylon@6.16.9': 606 | dependencies: 607 | '@types/babel-types': 7.0.16 608 | 609 | accepts@1.3.8: 610 | dependencies: 611 | mime-types: 2.1.35 612 | negotiator: 0.6.3 613 | 614 | acorn-globals@3.1.0: 615 | dependencies: 616 | acorn: 4.0.13 617 | 618 | acorn@3.3.0: {} 619 | 620 | acorn@4.0.13: {} 621 | 622 | align-text@0.1.4: 623 | dependencies: 624 | kind-of: 3.2.2 625 | longest: 1.0.1 626 | repeat-string: 1.6.1 627 | 628 | amdefine@1.0.1: {} 629 | 630 | append-field@1.0.0: {} 631 | 632 | array-flatten@1.1.1: {} 633 | 634 | asap@2.0.6: {} 635 | 636 | babel-runtime@6.26.0: 637 | dependencies: 638 | core-js: 2.6.12 639 | regenerator-runtime: 0.11.1 640 | 641 | babel-types@6.26.0: 642 | dependencies: 643 | babel-runtime: 6.26.0 644 | esutils: 2.0.3 645 | lodash: 4.17.21 646 | to-fast-properties: 1.0.3 647 | 648 | babylon@6.18.0: {} 649 | 650 | basic-auth@2.0.1: 651 | dependencies: 652 | safe-buffer: 5.1.2 653 | 654 | body-parser@1.18.3: 655 | dependencies: 656 | bytes: 3.0.0 657 | content-type: 1.0.5 658 | debug: 2.6.9 659 | depd: 1.1.2 660 | http-errors: 1.6.3 661 | iconv-lite: 0.4.23 662 | on-finished: 2.3.0 663 | qs: 6.5.2 664 | raw-body: 2.3.3 665 | type-is: 1.6.18 666 | transitivePeerDependencies: 667 | - supports-color 668 | 669 | buffer-from@1.1.2: {} 670 | 671 | busboy@1.6.0: 672 | dependencies: 673 | streamsearch: 1.1.0 674 | 675 | bytes@3.0.0: {} 676 | 677 | call-bind@1.0.7: 678 | dependencies: 679 | es-define-property: 1.0.0 680 | es-errors: 1.3.0 681 | function-bind: 1.1.2 682 | get-intrinsic: 1.2.4 683 | set-function-length: 1.2.2 684 | 685 | camelcase@1.2.1: {} 686 | 687 | center-align@0.1.3: 688 | dependencies: 689 | align-text: 0.1.4 690 | lazy-cache: 1.0.4 691 | 692 | character-parser@2.2.0: 693 | dependencies: 694 | is-regex: 1.1.4 695 | 696 | clean-css@3.4.28: 697 | dependencies: 698 | commander: 2.8.1 699 | source-map: 0.4.4 700 | 701 | cliui@2.1.0: 702 | dependencies: 703 | center-align: 0.1.3 704 | right-align: 0.1.3 705 | wordwrap: 0.0.2 706 | 707 | commander@2.8.1: 708 | dependencies: 709 | graceful-readlink: 1.0.1 710 | 711 | concat-stream@1.6.2: 712 | dependencies: 713 | buffer-from: 1.1.2 714 | inherits: 2.0.4 715 | readable-stream: 2.3.8 716 | typedarray: 0.0.6 717 | 718 | constantinople@3.1.2: 719 | dependencies: 720 | '@types/babel-types': 7.0.16 721 | '@types/babylon': 6.16.9 722 | babel-types: 6.26.0 723 | babylon: 6.18.0 724 | 725 | content-disposition@0.5.2: {} 726 | 727 | content-type@1.0.5: {} 728 | 729 | cookie-parser@1.4.7: 730 | dependencies: 731 | cookie: 0.7.2 732 | cookie-signature: 1.0.6 733 | 734 | cookie-signature@1.0.6: {} 735 | 736 | cookie@0.3.1: {} 737 | 738 | cookie@0.7.2: {} 739 | 740 | core-js@2.6.12: {} 741 | 742 | core-util-is@1.0.3: {} 743 | 744 | cors@2.8.5: 745 | dependencies: 746 | object-assign: 4.1.1 747 | vary: 1.1.2 748 | 749 | debug@2.6.9: 750 | dependencies: 751 | ms: 2.0.0 752 | 753 | decamelize@1.2.0: {} 754 | 755 | define-data-property@1.1.4: 756 | dependencies: 757 | es-define-property: 1.0.0 758 | es-errors: 1.3.0 759 | gopd: 1.0.1 760 | 761 | depd@1.1.2: {} 762 | 763 | destroy@1.0.4: {} 764 | 765 | doctypes@1.1.0: {} 766 | 767 | ee-first@1.1.1: {} 768 | 769 | encodeurl@1.0.2: {} 770 | 771 | es-define-property@1.0.0: 772 | dependencies: 773 | get-intrinsic: 1.2.4 774 | 775 | es-errors@1.3.0: {} 776 | 777 | escape-html@1.0.3: {} 778 | 779 | esutils@2.0.3: {} 780 | 781 | etag@1.8.1: {} 782 | 783 | express@4.16.4: 784 | dependencies: 785 | accepts: 1.3.8 786 | array-flatten: 1.1.1 787 | body-parser: 1.18.3 788 | content-disposition: 0.5.2 789 | content-type: 1.0.5 790 | cookie: 0.3.1 791 | cookie-signature: 1.0.6 792 | debug: 2.6.9 793 | depd: 1.1.2 794 | encodeurl: 1.0.2 795 | escape-html: 1.0.3 796 | etag: 1.8.1 797 | finalhandler: 1.1.1 798 | fresh: 0.5.2 799 | merge-descriptors: 1.0.1 800 | methods: 1.1.2 801 | on-finished: 2.3.0 802 | parseurl: 1.3.3 803 | path-to-regexp: 0.1.7 804 | proxy-addr: 2.0.7 805 | qs: 6.5.2 806 | range-parser: 1.2.1 807 | safe-buffer: 5.1.2 808 | send: 0.16.2 809 | serve-static: 1.13.2 810 | setprototypeof: 1.1.0 811 | statuses: 1.4.0 812 | type-is: 1.6.18 813 | utils-merge: 1.0.1 814 | vary: 1.1.2 815 | transitivePeerDependencies: 816 | - supports-color 817 | 818 | finalhandler@1.1.1: 819 | dependencies: 820 | debug: 2.6.9 821 | encodeurl: 1.0.2 822 | escape-html: 1.0.3 823 | on-finished: 2.3.0 824 | parseurl: 1.3.3 825 | statuses: 1.4.0 826 | unpipe: 1.0.0 827 | transitivePeerDependencies: 828 | - supports-color 829 | 830 | forwarded@0.2.0: {} 831 | 832 | fresh@0.5.2: {} 833 | 834 | fs-extra@11.2.0: 835 | dependencies: 836 | graceful-fs: 4.2.11 837 | jsonfile: 6.1.0 838 | universalify: 2.0.1 839 | 840 | function-bind@1.1.2: {} 841 | 842 | get-intrinsic@1.2.4: 843 | dependencies: 844 | es-errors: 1.3.0 845 | function-bind: 1.1.2 846 | has-proto: 1.0.3 847 | has-symbols: 1.0.3 848 | hasown: 2.0.2 849 | 850 | gopd@1.0.1: 851 | dependencies: 852 | get-intrinsic: 1.2.4 853 | 854 | graceful-fs@4.2.11: {} 855 | 856 | graceful-readlink@1.0.1: {} 857 | 858 | has-property-descriptors@1.0.2: 859 | dependencies: 860 | es-define-property: 1.0.0 861 | 862 | has-proto@1.0.3: {} 863 | 864 | has-symbols@1.0.3: {} 865 | 866 | has-tostringtag@1.0.2: 867 | dependencies: 868 | has-symbols: 1.0.3 869 | 870 | hasown@2.0.2: 871 | dependencies: 872 | function-bind: 1.1.2 873 | 874 | http-errors@1.6.3: 875 | dependencies: 876 | depd: 1.1.2 877 | inherits: 2.0.3 878 | setprototypeof: 1.1.0 879 | statuses: 1.5.0 880 | 881 | iconv-lite@0.4.23: 882 | dependencies: 883 | safer-buffer: 2.1.2 884 | 885 | inherits@2.0.3: {} 886 | 887 | inherits@2.0.4: {} 888 | 889 | ipaddr.js@1.9.1: {} 890 | 891 | is-buffer@1.1.6: {} 892 | 893 | is-core-module@2.15.1: 894 | dependencies: 895 | hasown: 2.0.2 896 | 897 | is-expression@3.0.0: 898 | dependencies: 899 | acorn: 4.0.13 900 | object-assign: 4.1.1 901 | 902 | is-promise@2.2.2: {} 903 | 904 | is-regex@1.1.4: 905 | dependencies: 906 | call-bind: 1.0.7 907 | has-tostringtag: 1.0.2 908 | 909 | isarray@1.0.0: {} 910 | 911 | js-stringify@1.0.2: {} 912 | 913 | jsonfile@6.1.0: 914 | dependencies: 915 | universalify: 2.0.1 916 | optionalDependencies: 917 | graceful-fs: 4.2.11 918 | 919 | jstransformer@1.0.0: 920 | dependencies: 921 | is-promise: 2.2.2 922 | promise: 7.3.1 923 | 924 | kind-of@3.2.2: 925 | dependencies: 926 | is-buffer: 1.1.6 927 | 928 | lazy-cache@1.0.4: {} 929 | 930 | lodash@4.17.21: {} 931 | 932 | longest@1.0.1: {} 933 | 934 | media-typer@0.3.0: {} 935 | 936 | merge-descriptors@1.0.1: {} 937 | 938 | methods@1.1.2: {} 939 | 940 | mime-db@1.52.0: {} 941 | 942 | mime-types@2.1.35: 943 | dependencies: 944 | mime-db: 1.52.0 945 | 946 | mime@1.4.1: {} 947 | 948 | minimist@1.2.8: {} 949 | 950 | mkdirp@0.5.6: 951 | dependencies: 952 | minimist: 1.2.8 953 | 954 | morgan@1.9.1: 955 | dependencies: 956 | basic-auth: 2.0.1 957 | debug: 2.6.9 958 | depd: 1.1.2 959 | on-finished: 2.3.0 960 | on-headers: 1.0.2 961 | transitivePeerDependencies: 962 | - supports-color 963 | 964 | ms@2.0.0: {} 965 | 966 | multer@1.4.5-lts.1: 967 | dependencies: 968 | append-field: 1.0.0 969 | busboy: 1.6.0 970 | concat-stream: 1.6.2 971 | mkdirp: 0.5.6 972 | object-assign: 4.1.1 973 | type-is: 1.6.18 974 | xtend: 4.0.2 975 | 976 | negotiator@0.6.3: {} 977 | 978 | object-assign@4.1.1: {} 979 | 980 | on-finished@2.3.0: 981 | dependencies: 982 | ee-first: 1.1.1 983 | 984 | on-headers@1.0.2: {} 985 | 986 | parseurl@1.3.3: {} 987 | 988 | path-parse@1.0.7: {} 989 | 990 | path-to-regexp@0.1.7: {} 991 | 992 | process-nextick-args@2.0.1: {} 993 | 994 | promise@7.3.1: 995 | dependencies: 996 | asap: 2.0.6 997 | 998 | proxy-addr@2.0.7: 999 | dependencies: 1000 | forwarded: 0.2.0 1001 | ipaddr.js: 1.9.1 1002 | 1003 | pug-attrs@2.0.4: 1004 | dependencies: 1005 | constantinople: 3.1.2 1006 | js-stringify: 1.0.2 1007 | pug-runtime: 2.0.5 1008 | 1009 | pug-code-gen@1.1.1: 1010 | dependencies: 1011 | constantinople: 3.1.2 1012 | doctypes: 1.1.0 1013 | js-stringify: 1.0.2 1014 | pug-attrs: 2.0.4 1015 | pug-error: 1.3.3 1016 | pug-runtime: 2.0.5 1017 | void-elements: 2.0.1 1018 | with: 5.1.1 1019 | 1020 | pug-error@1.3.3: {} 1021 | 1022 | pug-filters@2.1.5: 1023 | dependencies: 1024 | clean-css: 3.4.28 1025 | constantinople: 3.1.2 1026 | jstransformer: 1.0.0 1027 | pug-error: 1.3.3 1028 | pug-walk: 1.1.8 1029 | resolve: 1.22.8 1030 | uglify-js: 2.8.29 1031 | 1032 | pug-lexer@3.1.0: 1033 | dependencies: 1034 | character-parser: 2.2.0 1035 | is-expression: 3.0.0 1036 | pug-error: 1.3.3 1037 | 1038 | pug-linker@2.0.3: 1039 | dependencies: 1040 | pug-error: 1.3.3 1041 | pug-walk: 1.1.8 1042 | 1043 | pug-load@2.0.12: 1044 | dependencies: 1045 | object-assign: 4.1.1 1046 | pug-walk: 1.1.8 1047 | 1048 | pug-parser@2.0.2: 1049 | dependencies: 1050 | pug-error: 1.3.3 1051 | token-stream: 0.0.1 1052 | 1053 | pug-runtime@2.0.5: {} 1054 | 1055 | pug-strip-comments@1.0.4: 1056 | dependencies: 1057 | pug-error: 1.3.3 1058 | 1059 | pug-walk@1.1.8: {} 1060 | 1061 | pug@2.0.0-beta11: 1062 | dependencies: 1063 | pug-code-gen: 1.1.1 1064 | pug-filters: 2.1.5 1065 | pug-lexer: 3.1.0 1066 | pug-linker: 2.0.3 1067 | pug-load: 2.0.12 1068 | pug-parser: 2.0.2 1069 | pug-runtime: 2.0.5 1070 | pug-strip-comments: 1.0.4 1071 | 1072 | qs@6.5.2: {} 1073 | 1074 | range-parser@1.2.1: {} 1075 | 1076 | raw-body@2.3.3: 1077 | dependencies: 1078 | bytes: 3.0.0 1079 | http-errors: 1.6.3 1080 | iconv-lite: 0.4.23 1081 | unpipe: 1.0.0 1082 | 1083 | readable-stream@2.3.8: 1084 | dependencies: 1085 | core-util-is: 1.0.3 1086 | inherits: 2.0.4 1087 | isarray: 1.0.0 1088 | process-nextick-args: 2.0.1 1089 | safe-buffer: 5.1.2 1090 | string_decoder: 1.1.1 1091 | util-deprecate: 1.0.2 1092 | 1093 | regenerator-runtime@0.11.1: {} 1094 | 1095 | repeat-string@1.6.1: {} 1096 | 1097 | resolve@1.22.8: 1098 | dependencies: 1099 | is-core-module: 2.15.1 1100 | path-parse: 1.0.7 1101 | supports-preserve-symlinks-flag: 1.0.0 1102 | 1103 | right-align@0.1.3: 1104 | dependencies: 1105 | align-text: 0.1.4 1106 | 1107 | safe-buffer@5.1.2: {} 1108 | 1109 | safer-buffer@2.1.2: {} 1110 | 1111 | send@0.16.2: 1112 | dependencies: 1113 | debug: 2.6.9 1114 | depd: 1.1.2 1115 | destroy: 1.0.4 1116 | encodeurl: 1.0.2 1117 | escape-html: 1.0.3 1118 | etag: 1.8.1 1119 | fresh: 0.5.2 1120 | http-errors: 1.6.3 1121 | mime: 1.4.1 1122 | ms: 2.0.0 1123 | on-finished: 2.3.0 1124 | range-parser: 1.2.1 1125 | statuses: 1.4.0 1126 | transitivePeerDependencies: 1127 | - supports-color 1128 | 1129 | serve-static@1.13.2: 1130 | dependencies: 1131 | encodeurl: 1.0.2 1132 | escape-html: 1.0.3 1133 | parseurl: 1.3.3 1134 | send: 0.16.2 1135 | transitivePeerDependencies: 1136 | - supports-color 1137 | 1138 | set-function-length@1.2.2: 1139 | dependencies: 1140 | define-data-property: 1.1.4 1141 | es-errors: 1.3.0 1142 | function-bind: 1.1.2 1143 | get-intrinsic: 1.2.4 1144 | gopd: 1.0.1 1145 | has-property-descriptors: 1.0.2 1146 | 1147 | setprototypeof@1.1.0: {} 1148 | 1149 | source-map@0.4.4: 1150 | dependencies: 1151 | amdefine: 1.0.1 1152 | 1153 | source-map@0.5.7: {} 1154 | 1155 | statuses@1.4.0: {} 1156 | 1157 | statuses@1.5.0: {} 1158 | 1159 | streamsearch@1.1.0: {} 1160 | 1161 | string_decoder@1.1.1: 1162 | dependencies: 1163 | safe-buffer: 5.1.2 1164 | 1165 | supports-preserve-symlinks-flag@1.0.0: {} 1166 | 1167 | to-fast-properties@1.0.3: {} 1168 | 1169 | token-stream@0.0.1: {} 1170 | 1171 | type-is@1.6.18: 1172 | dependencies: 1173 | media-typer: 0.3.0 1174 | mime-types: 2.1.35 1175 | 1176 | typedarray@0.0.6: {} 1177 | 1178 | uglify-js@2.8.29: 1179 | dependencies: 1180 | source-map: 0.5.7 1181 | yargs: 3.10.0 1182 | optionalDependencies: 1183 | uglify-to-browserify: 1.0.2 1184 | 1185 | uglify-to-browserify@1.0.2: 1186 | optional: true 1187 | 1188 | universalify@2.0.1: {} 1189 | 1190 | unpipe@1.0.0: {} 1191 | 1192 | util-deprecate@1.0.2: {} 1193 | 1194 | utils-merge@1.0.1: {} 1195 | 1196 | vary@1.1.2: {} 1197 | 1198 | void-elements@2.0.1: {} 1199 | 1200 | window-size@0.1.0: {} 1201 | 1202 | with@5.1.1: 1203 | dependencies: 1204 | acorn: 3.3.0 1205 | acorn-globals: 3.1.0 1206 | 1207 | wordwrap@0.0.2: {} 1208 | 1209 | xtend@4.0.2: {} 1210 | 1211 | yargs@3.10.0: 1212 | dependencies: 1213 | camelcase: 1.2.1 1214 | cliui: 2.1.0 1215 | decamelize: 1.2.0 1216 | window-size: 0.1.0 1217 | -------------------------------------------------------------------------------- /server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | let express = require("express"); 2 | let router = express.Router(); 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | 6 | const multer = require("multer"); 7 | 8 | const dir = path.resolve(__dirname, "../uploads"); 9 | 10 | const storage = multer.memoryStorage(); 11 | const upload = multer({ storage }); 12 | 13 | const wait = (n) => 14 | new Promise((r) => { 15 | setTimeout(() => { 16 | r(); 17 | }, n); 18 | }); 19 | 20 | // 获取分片 21 | router.get("/chunks", async (req, res) => { 22 | try { 23 | if (!req.query.hash) { 24 | return res.json({ 25 | code: 400, 26 | data: [], 27 | message: "", 28 | }); 29 | } 30 | 31 | // await wait(5000); 32 | const { hash } = req.query; 33 | 34 | const fullPath = path.resolve(dir, hash); 35 | const r = fs.existsSync(fullPath); 36 | 37 | if (!r) 38 | return res.json({ 39 | code: 200, 40 | message: "", 41 | data: [], 42 | }); 43 | 44 | const chunks = fs.readdirSync(fullPath, { 45 | encoding: "utf8", 46 | withFileTypes: true, 47 | }); 48 | 49 | res.json({ 50 | code: 200, 51 | message: "success", 52 | data: chunks.map((v) => v.name), 53 | }); 54 | } catch (err) { 55 | console.log("err", err.message); 56 | } 57 | }); 58 | 59 | // 保存分片 60 | router.post("/uploadchunk", upload.single("chunk"), async (req, res) => { 61 | if (!req.file) { 62 | return res.json({ 63 | code: 400, 64 | message: "文件不存在", 65 | }); 66 | } 67 | const writeFileStream = () => 68 | new Promise((resolve, reject) => { 69 | let currentDir = path.resolve(dir, req.body.dirname); 70 | const exist = fs.existsSync(currentDir); 71 | if (!exist) { 72 | fs.mkdirSync(currentDir, { 73 | recursive: true, 74 | }); 75 | } 76 | const regex = /(?<=\_)\d+(?=\.)/; 77 | const r = regex.exec(req.body.chunkname); 78 | const i = +r[0]; 79 | // 测试条件{n}个切片失败 80 | if (0) { 81 | return res.json({ 82 | code: 400, 83 | message: "上传失败", 84 | }); 85 | } 86 | const fullPath = path.resolve(dir, req.body.dirname, req.body.chunkname); 87 | const writeStream = fs.createWriteStream(fullPath); 88 | writeStream.write(req.file.buffer); 89 | writeStream.on("finish", () => { 90 | writeStream.close(); 91 | resolve(); 92 | }); 93 | 94 | writeStream.on("error", (err) => { 95 | writeStream.close(); 96 | reject(err); 97 | }); 98 | resolve(); 99 | }); 100 | 101 | try { 102 | await writeFileStream(); 103 | } catch (err) { 104 | console.log("err", err); 105 | return res.json({ 106 | code: 200, 107 | message: "分片保存失败", 108 | }); 109 | } 110 | console.log("res"); 111 | res.json({ 112 | code: 200, 113 | message: "分片保存成功", 114 | }); 115 | }); 116 | 117 | const localRmdirAll = (fullPath) => { 118 | const list = fs.readdirSync(fullPath, { 119 | withFileTypes: true, 120 | }); 121 | const func = (files, fullPath) => { 122 | files.forEach((v) => { 123 | if (v.isDirectory()) { 124 | const currentPath = path.resolve(v.path, v.name); 125 | const L = fs.readdirSync(currentPath, { 126 | withFileTypes: true, 127 | }); 128 | if (L.length === 0) return; 129 | func(L, currentPath); 130 | } else { 131 | const currentPath = path.resolve(v.path, v.name); 132 | fs.unlinkSync(currentPath); 133 | } 134 | }); 135 | }; 136 | func(list); 137 | fs.rmdirSync(fullPath); 138 | }; 139 | // 合并分片 140 | router.post("/mergechunks", async (req, res) => { 141 | const { hash, filename, chunkcount } = req.body; 142 | if (!hash || !filename || typeof chunkcount !== "number") { 143 | return res.json({ 144 | code: 400, 145 | message: "合并失败", 146 | }); 147 | } 148 | 149 | // 把上传的切片删除 150 | if (1) { 151 | const rootPath = path.resolve(__dirname, `../uploads/${hash}`); 152 | localRmdirAll(rootPath); 153 | return res.json({ 154 | code: 200, 155 | message: "成功", 156 | }); 157 | } 158 | 159 | const fullDir = path.resolve(dir, hash); 160 | const exist = fs.existsSync(fullDir); 161 | if (!exist) 162 | return res.json({ 163 | code: 400, 164 | message: "合并失败", 165 | }); 166 | 167 | const findLastIndex = (str) => { 168 | const regex = /(?<=\_)\d+(?=\.)/; 169 | const r = regex.exec(str); 170 | return +r[0]; 171 | }; 172 | const chunks = fs 173 | .readdirSync(fullDir) 174 | .sort((a, b) => { 175 | const a_i = findLastIndex(a); 176 | const b_i = findLastIndex(b); 177 | return a_i - b_i; 178 | }) 179 | .map((v) => path.resolve(fullDir, v)); 180 | console.log("chunks", chunks); 181 | const targetUrl = path.resolve(dir, filename); 182 | let total = chunks.length; 183 | 184 | // 校验分片一致性 185 | console.log("校验一致性"); 186 | if (chunkcount !== total) 187 | return res.json({ 188 | code: 400, 189 | message: "合并失败", 190 | }); 191 | 192 | // 判断是否存在 193 | const isflag = fs.existsSync(targetUrl); 194 | if (isflag) { 195 | fs.rmSync(targetUrl); 196 | } 197 | 198 | const merge = () => 199 | new Promise(async (resolve, reject) => { 200 | const writeStream = fs.createWriteStream(targetUrl, { 201 | flags: "a", 202 | }); // 使用追加模式 'a' 来合并文件 203 | const func = (task) => 204 | new Promise((resolve, reject) => { 205 | const readStream = fs.createReadStream(task); 206 | 207 | readStream.on("end", () => { 208 | resolve(); 209 | }); 210 | readStream.on("error", (err) => { 211 | console.error("Error reading stream:", err); 212 | reject(err); 213 | }); 214 | 215 | // 确保不会关闭 writeStream 直到所有切片都写入完毕 216 | readStream.pipe(writeStream, { end: false }); 217 | }); 218 | 219 | try { 220 | for (const v of chunks) { 221 | await func(v); 222 | } 223 | // 所有切片都写入后关闭 writeStream 224 | writeStream.end(); 225 | writeStream.on("finish", () => { 226 | resolve(); 227 | }); 228 | writeStream.on("error", (err) => { 229 | console.error("Error writing to stream:", err); 230 | reject(err); 231 | }); 232 | } catch (err) { 233 | // 如果在合并过程中发生错误,关闭 writeStream 并拒绝 Promise 234 | writeStream.end(); 235 | reject(err); 236 | } 237 | }); 238 | await merge(); 239 | 240 | const unlinks = () => { 241 | for (const v of chunks) { 242 | try { 243 | fs.unlinkSync(v, { 244 | recursive: true, 245 | force: true, 246 | }); 247 | } catch (err) { 248 | console.log("err", err); 249 | } 250 | } 251 | }; 252 | 253 | unlinks(); 254 | 255 | res.json({ 256 | code: 200, 257 | message: "合并成功", 258 | }); 259 | }); 260 | 261 | module.exports = router; 262 | -------------------------------------------------------------------------------- /server/views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /server/views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /server/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | import { setupUploader } from "./upload"; 3 | 4 | document.querySelector("#app")!.innerHTML = ` 5 |
6 | `; 7 | 8 | setupUploader(); 9 | -------------------------------------------------------------------------------- /src/packages/constant.ts: -------------------------------------------------------------------------------- 1 | export enum UPLOAD_ERROR_STATES { 2 | GET_CHUNKS_ERROR = "get-chunks-error", 3 | UPLOAD_CHUNK_ERROR = "upload-chunk-error", 4 | MERGE_CHUNKS_ERROR = "merge-chunks-error", 5 | CREATE_HASH_ERROR = "create-hash-error", 6 | CREATE_CHUNK_ERROR = "create-chunk-error", 7 | FILE_ERROR = "file-error", 8 | TERMINATE_UPLOAD = "terminate-upload", 9 | } 10 | -------------------------------------------------------------------------------- /src/packages/error/index.ts: -------------------------------------------------------------------------------- 1 | // retain it 2 | import {callTypeof} from '@/shared'; 3 | type ErrorParams = Parameters; 4 | type TypeErrorParams = Parameters; 5 | import type {ErrorType} from '@/types'; 6 | 7 | export function throwError(message:ErrorParams[0],options?:ErrorParams[1]):void { 8 | throw new Error(message,options); 9 | } 10 | 11 | export function throwTypeError(...args:TypeErrorParams) { 12 | const message = args[0]; 13 | const options = args[1]; 14 | throw new TypeError(message,options); 15 | } 16 | 17 | export function examineType(target:T,expectType:'object'|'array'|'string'|'number'|'null'|'bigint'|'symbol'|'function') { 18 | const r_type = callTypeof(target); 19 | return r_type === expectType 20 | }; 21 | 22 | export class UxPlusUploderError extends Error { 23 | type:ErrorType; 24 | constructor(message: string,type:ErrorType) { 25 | super(message); 26 | this.name = "UxPlusUploderError"; 27 | this.type = type; 28 | if (Error.captureStackTrace) { 29 | Error.captureStackTrace(this, UxPlusUploderError); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/packages/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useControl } from "./useControl"; 2 | import { useNextTick } from "./useNextTick"; 3 | import { useStoreTerminable } from "./useStoreTerminable"; 4 | 5 | export { 6 | useControl, 7 | useNextTick, 8 | useStoreTerminable 9 | } -------------------------------------------------------------------------------- /src/packages/hooks/types.ts: -------------------------------------------------------------------------------- 1 | export type * from './useNextTick/types'; 2 | export type * from './useStoreTerminable/types'; -------------------------------------------------------------------------------- /src/packages/hooks/useControl/index.ts: -------------------------------------------------------------------------------- 1 | export const useControl = () => { 2 | const map:Map = new Map; 3 | const insert = (url:string,abort:()=>void)=>{ 4 | map.set(url,abort); 5 | }; 6 | 7 | const aborts = ()=>{ 8 | map.forEach(v=>v()); 9 | map.clear(); 10 | } 11 | 12 | const del = (url:string) =>{ 13 | map.delete(url); 14 | } 15 | 16 | const clear = () => { 17 | map.clear(); 18 | } 19 | 20 | 21 | return { 22 | aborts, 23 | del, 24 | insert, 25 | clear 26 | } 27 | }; -------------------------------------------------------------------------------- /src/packages/hooks/useNextTick/index.ts: -------------------------------------------------------------------------------- 1 | import type {UseNextTick} from './types'; 2 | export const useNextTick:UseNextTick = () => { 3 | const win = window; 4 | if (Promise.resolve) { 5 | return (callback) => { 6 | Promise.resolve().then(callback); 7 | }; 8 | } else if (win.queueMicrotask) { 9 | return (callback) => { 10 | win.queueMicrotask(callback); 11 | }; 12 | // @ts-ignore 13 | } else if(win.setImmediate) { 14 | return (callback) => { 15 | // @ts-ignore 16 | win.setImmediate(callback); 17 | }; 18 | } 19 | return (callback) => { 20 | win.setTimeout(callback,0); 21 | }; 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /src/packages/hooks/useNextTick/types.ts: -------------------------------------------------------------------------------- 1 | type Callback = () => void; 2 | export interface UseNextTick { 3 | ():(callback: Callback) => void 4 | } -------------------------------------------------------------------------------- /src/packages/hooks/useStoreTerminable/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Terminable, 3 | } from './types'; 4 | import { useNextTick } from '../useNextTick/index'; 5 | 6 | const $nextTick = useNextTick(); 7 | 8 | export const useStoreTerminable = () => { 9 | const obj:Terminable = { 10 | task:[], 11 | state:false, 12 | execute() { 13 | this.state = true; 14 | this.task.forEach(t=>t()); 15 | }, 16 | push(t) { 17 | this.task.push(t); 18 | }, 19 | clear() { 20 | this.task.length = 0; 21 | $nextTick(()=>{ 22 | this.state = false; 23 | }) 24 | } 25 | } 26 | return obj; 27 | } 28 | -------------------------------------------------------------------------------- /src/packages/hooks/useStoreTerminable/types.ts: -------------------------------------------------------------------------------- 1 | export type TerminableTaskItem = ()=>void; 2 | export interface Terminable { 3 | task:TerminableTaskItem[]; 4 | state:boolean; 5 | execute:()=>void; 6 | push:(t:TerminableTaskItem) =>void; 7 | clear:()=>void 8 | } -------------------------------------------------------------------------------- /src/packages/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import type { 3 | Options, 4 | ChunkUploader, 5 | UploadChunks, 6 | RawFile, 7 | UploaderConfig, 8 | RequestFunc, 9 | UploadError, 10 | Chunks, 11 | PrivateChunkUploaderProps, 12 | DeepPartial, 13 | SetParamsAssemble, 14 | ERROR_RESPOSE, 15 | _GetChunks, 16 | } from "./types"; 17 | import { 18 | headers, 19 | objFor, 20 | callReject, 21 | isExistF, 22 | handleCanceledError, 23 | } from "./shared"; 24 | import { useControl, useNextTick, useStoreTerminable } from "./hooks"; 25 | import { 26 | createChunks, 27 | clearRuntimeProduct, 28 | createHash, 29 | } from "./modules/handles"; 30 | import { UPLOAD_ERROR_STATES } from "./constant"; 31 | import type { Terminable, TerminableTaskItem } from "./hooks/types"; 32 | export type { ERROR_RESPOSE, Options }; 33 | 34 | const initOptions = () => { 35 | const localOptions: DeepPartial = { 36 | requestConfig: { 37 | instance: axios.create(), 38 | }, 39 | uploadConfig: { 40 | chunkSize: 100, 41 | max: 100, 42 | limit: 5, 43 | }, 44 | }; 45 | return localOptions as Options; 46 | }; 47 | 48 | const setParamsAssemble: SetParamsAssemble = { 49 | getChunks(hash) { 50 | return { 51 | hash, 52 | }; 53 | }, 54 | uploadChunk(params) { 55 | return params; 56 | }, 57 | merge(params) { 58 | return params; 59 | }, 60 | }; 61 | 62 | /** 63 | * 64 | * @class UxChunkUploader 65 | * @typedef {UxChunkUploader} 66 | * @implements {ChunkUploader} 67 | */ 68 | class UxChunkUploader implements ChunkUploader { 69 | options: Options | null = null; 70 | private _chunkcount: number = 0; 71 | private _progressGather: PrivateChunkUploaderProps["progressGather"] = {}; 72 | private _requestFunc: PrivateChunkUploaderProps["requestFunc"] = null; 73 | private _control?: PrivateChunkUploaderProps["control"]; 74 | $nextTick = useNextTick(); 75 | private _terminable: Terminable = useStoreTerminable(); 76 | private _mergechunkmutex = false; 77 | constructor(options: Options) { 78 | this.setOptions(options); 79 | } 80 | setOptions(options: DeepPartial) { 81 | if (!options) throw new TypeError("options is not a valid type"); 82 | // 初始化默认配置 83 | !this.options && (this.options = initOptions()); 84 | const opt = Object.assign(this.options, options) as Required; 85 | this.options = opt; 86 | const requestConfig = this.options!.requestConfig!; 87 | if (!requestConfig) return void 0; 88 | !requestConfig.instance && (requestConfig.instance = axios); 89 | 90 | // 初始化请求函数 91 | const initRequester = () => { 92 | const context = this; 93 | const getRequestConfig = () => context.options!.requestConfig; 94 | // 上传切片函数 95 | const uploadChunk: RequestFunc["uploadChunk"] = ({ 96 | formData, 97 | url, 98 | taskIndex, 99 | }) => 100 | new Promise((resolve, reject: (e: ERROR_RESPOSE) => void) => { 101 | const control = new AbortController(); 102 | const _control = context._control!; 103 | const key = `${url}_${taskIndex}`; 104 | _control.insert(key, control.abort.bind(control)); 105 | getRequestConfig().instance!<{ 106 | code: number; 107 | data: any; 108 | }>({ 109 | method: "POST", 110 | data: formData, 111 | url, 112 | headers, 113 | signal: control.signal, 114 | }) 115 | .then((r) => { 116 | if (getRequestConfig().uploadChunk.onResponse) { 117 | return resolve(getRequestConfig()!.uploadChunk.onResponse!(r)); 118 | } 119 | resolve(r.data.code); 120 | }) 121 | .finally(() => { 122 | _control.del(key); 123 | }) 124 | .catch(reject); 125 | }); 126 | const localRequestFunc: RequestFunc = { 127 | merge(url, params) { 128 | const control = new AbortController(); 129 | const f = isExistF( 130 | setParamsAssemble.merge, 131 | context.options!.requestConfig.merge.setParams 132 | ); 133 | const _control = context._control!; 134 | _control.insert(url, control.abort.bind(control)); 135 | const data = f(params); 136 | return getRequestConfig().instance!({ 137 | method: "POST", 138 | url, 139 | data, 140 | signal: control.signal, 141 | }).then((r) => { 142 | if (getRequestConfig().merge?.onResponse) 143 | return Promise.resolve(getRequestConfig().merge!.onResponse!(r)); 144 | return Promise.resolve(r.data.code); 145 | }); 146 | }, 147 | getChunk(url: string, hash: string) { 148 | const control = new AbortController(); 149 | const _control = context._control!; 150 | _control.insert(url, control.abort.bind(control)); 151 | const f = isExistF( 152 | setParamsAssemble.getChunks, 153 | getRequestConfig().getChunks?.setParams 154 | ); 155 | return requestConfig.instance!<{ 156 | code: number; 157 | data: any; 158 | }>({ 159 | method: "get", 160 | url, 161 | signal: control.signal, 162 | params: f(hash), 163 | }).then((r) => { 164 | let isError = false; 165 | if (getRequestConfig().getChunks?.onResponse) { 166 | const authenticResponse = 167 | getRequestConfig().getChunks!.onResponse!(r); 168 | if (authenticResponse.code !== 200) isError = true; 169 | } else if (r.data.code !== 200) isError = true; 170 | if (isError) { 171 | return callReject({ 172 | type: UPLOAD_ERROR_STATES.GET_CHUNKS_ERROR, 173 | reason: "The status code of getChunk is not 200", 174 | }); 175 | } 176 | return Promise.resolve(r.data.data); 177 | }); 178 | }, 179 | uploadChunk, 180 | }; 181 | this._requestFunc = localRequestFunc; 182 | }; 183 | initRequester(); 184 | } 185 | private _setOptions(options: Record) { 186 | Object.assign(this, options); 187 | } 188 | private _uploadChunks({ 189 | hash, 190 | chunks, 191 | limit = 4, 192 | filename, 193 | onProgress, 194 | count: localCount, 195 | }: Parameters[0]) { 196 | let count = localCount; 197 | const createErrorState = () => { 198 | const isError = { 199 | value: false, 200 | }; 201 | const setError = () => { 202 | isError.value = true; 203 | }; 204 | return { 205 | isError, 206 | setError, 207 | }; 208 | }; 209 | const { isError, setError } = createErrorState(); 210 | const chks: Chunks = chunks; 211 | const context = this; 212 | context._mergechunkmutex = true; 213 | // 上传切片 214 | const upload = () => 215 | new Promise( 216 | ( 217 | resolve: (value: undefined) => void, 218 | reject: (err: Parameters[0]) => void 219 | ) => { 220 | // 没有分片直接结束去合并 221 | if (!chks.length) { 222 | return resolve(void 0); 223 | } 224 | const handleProgress = (v: number, chunkname: string) => { 225 | const r = context.computedProgress(v, chunkname); 226 | typeof onProgress === "function" ? onProgress(r) : void 0; 227 | }; 228 | const chunksMap = chks.map((...args) => args[1]); 229 | const isRun = () => count !== 0 && !context._terminable.state; 230 | const f = isExistF( 231 | setParamsAssemble.uploadChunk, 232 | this.options!.requestConfig.uploadChunk.setParams 233 | ); 234 | 235 | const useSharedError = (err: any, taskIndex: number) => ({ 236 | type: UPLOAD_ERROR_STATES.UPLOAD_CHUNK_ERROR, 237 | reason: `The index:${taskIndex} slice file upload failed`, 238 | err, 239 | }); 240 | 241 | const useError = () => { 242 | reject({ 243 | type: UPLOAD_ERROR_STATES.TERMINATE_UPLOAD, 244 | reason: `${UPLOAD_ERROR_STATES.UPLOAD_CHUNK_ERROR}:terminated upload`, 245 | }); 246 | }; 247 | // 上传切片 248 | const run = () => { 249 | const taskIndex = chunksMap.shift(); 250 | if (context._terminable.state) 251 | return reject({ 252 | type: UPLOAD_ERROR_STATES.TERMINATE_UPLOAD, 253 | reason: "terminated upload", 254 | }); 255 | if (typeof taskIndex !== "number" || isError.value) return void 0; 256 | const v = chks[taskIndex!]; 257 | const useFormDate = () => { 258 | const formData = new FormData(); 259 | const formList = f({ 260 | chunk: v.chunk, 261 | chunkname: v.chunkname, 262 | dirname: hash, 263 | }); 264 | objFor(formList, (v, k) => formData.append(k, v)); 265 | return formData; 266 | }; 267 | const formData = useFormDate(); 268 | context 269 | ._requestFunc!.uploadChunk({ 270 | formData, 271 | url: context.options!.requestConfig!.uploadChunk.url, 272 | taskIndex, 273 | }) 274 | // 当有一个分片上传失败整个Promise任务都是失败的 275 | .then((code) => { 276 | if (code === 200) { 277 | handleProgress(1, v.chunkname); 278 | return Promise.resolve(void 0); 279 | } 280 | handleProgress(0, v.chunkname); 281 | setError(); 282 | reject(useSharedError(void 0, taskIndex)); 283 | }) 284 | .then(() => { 285 | context._terminable.state && useError(); 286 | count--; 287 | isRun() && run(); 288 | }) 289 | .catch((err) => { 290 | handleProgress(0, v.chunkname); 291 | return callReject(useSharedError(err, taskIndex)); 292 | }) 293 | .catch(reject) 294 | .finally(() => { 295 | // 判断是否上传完毕 296 | if ( 297 | count === 0 && 298 | context._mergechunkmutex && 299 | !isError.value && 300 | !context._terminable.state 301 | ) { 302 | context._mergechunkmutex = false; 303 | return resolve(void 0); 304 | } 305 | }); 306 | }; 307 | new Array(limit).fill(null).forEach(() => { 308 | isRun() && run(); 309 | }); 310 | } 311 | ); 312 | return upload() 313 | .then(() => 314 | Promise.resolve({ 315 | chunklength: chunks.length, 316 | filename, 317 | hash, 318 | }) 319 | ) 320 | .catch((err) => handleCanceledError(err, () => Promise.reject(err))); 321 | } 322 | // 合并 323 | private _merge(res: { hash: string; chunklength: number; filename: string }) { 324 | const context = this; 325 | const handleError = () => 326 | callReject({ 327 | type: UPLOAD_ERROR_STATES.MERGE_CHUNKS_ERROR, 328 | reason: "Failed to merge slices", 329 | }); 330 | return context 331 | ._requestFunc!.merge(context.options!.requestConfig!.merge.url, { 332 | hash: res.hash, 333 | chunkcount: res.chunklength, 334 | filename: res.filename, 335 | }) 336 | .then((code) => { 337 | if (code === 200) return void 0; 338 | return handleError(); 339 | }) 340 | .catch(() => handleError()); 341 | } 342 | // 上传 343 | public triggerUpload(file: RawFile, opt?: Partial) { 344 | const terminatedUpload = () => { 345 | this._terminable.execute(); 346 | }; 347 | if (!file) 348 | return { 349 | response: callReject({ 350 | err: void 0, 351 | reason: "file is not a valid value", 352 | type: UPLOAD_ERROR_STATES.FILE_ERROR, 353 | }), 354 | terminate: terminatedUpload, 355 | }; 356 | const context = this; 357 | const control = useControl(); 358 | const init = () => { 359 | context._terminable.clear(); 360 | context._control = control; 361 | clearRuntimeProduct(context._setOptions.bind(context)); 362 | }; 363 | init(); 364 | const data: Promise = new Promise( 365 | (resolve: (v: undefined) => void, reject: (e: UploadError) => void) => { 366 | const localOpt = Object.assign( 367 | { ...context.options!.uploadConfig }, 368 | { ...opt } 369 | ); 370 | const addTerminable = (abort: TerminableTaskItem) => { 371 | context._terminable.push(abort); 372 | }; 373 | createHash(file, addTerminable) 374 | .then((r) => { 375 | const { hash, suffix, filename } = r; 376 | const { err, chunks } = createChunks( 377 | { 378 | raw: file, 379 | hash, 380 | suffix, 381 | chunkSize: localOpt.chunkSize!, 382 | max: localOpt.max!, 383 | }, 384 | () => context._terminable.state, 385 | context._setOptions.bind(context) 386 | ); 387 | if (err) { 388 | return err; 389 | } 390 | return { 391 | chunks, 392 | filename, 393 | hash, 394 | }; 395 | }) 396 | .then(({ chunks, filename, hash }) => { 397 | context._terminable.push(() => control.aborts()); 398 | const isGetChunks = context.options?.requestConfig.getChunks?.url; 399 | const sharedValue = () => ({ 400 | chunks, 401 | hash, 402 | filename, 403 | }); 404 | if (isGetChunks) return context._getChunks(sharedValue()); 405 | return Promise.resolve({ 406 | ...sharedValue(), 407 | count: context._chunkcount, 408 | }); 409 | }) 410 | .then(({ hash, chunks, filename, count }) => 411 | context._uploadChunks({ 412 | hash, 413 | chunks, 414 | limit: localOpt.limit!, 415 | filename, 416 | onProgress: 417 | localOpt.onProgress || 418 | context.options?.uploadConfig?.onProgress, 419 | count, 420 | }) 421 | ) 422 | .then((r) => { 423 | const t = r!; 424 | return context._merge(t); 425 | }) 426 | .then(() => resolve(void 0)) 427 | .catch(reject); 428 | } 429 | ); 430 | return { 431 | terminate: terminatedUpload, 432 | response: data, 433 | }; 434 | } 435 | private computedProgress(progress: number, chunkname: string) { 436 | const context = this; 437 | context._progressGather[chunkname] = progress!; 438 | const refeef = context._progressGather; 439 | const chunkcount = context._chunkcount; 440 | let sum = 0; 441 | objFor(refeef, (v) => { 442 | sum += v; 443 | }); 444 | return Math.floor((sum / chunkcount) * 100); 445 | } 446 | // 获取切片 447 | private _getChunks({ 448 | hash, 449 | chunks, 450 | filename, 451 | }: Parameters<_GetChunks>[0]): ReturnType<_GetChunks> { 452 | const context = this; 453 | let chks = chunks; 454 | let count = chunks.length; 455 | return new Promise( 456 | ( 457 | resolve: (value: Awaited>) => void, 458 | reject: (reason?: any) => void 459 | ) => { 460 | const success = () => { 461 | resolve({ 462 | hash, 463 | chunks: chks, 464 | count, 465 | filename, 466 | }); 467 | }; 468 | if (!context._requestFunc!.getChunk) { 469 | chunks.forEach((v) => { 470 | context._progressGather[v.chunkname] = 0; 471 | }); 472 | success(); 473 | } 474 | // 获取已缓存的切片 475 | context 476 | ._requestFunc!.getChunk( 477 | context.options!.requestConfig!.getChunks!.url, 478 | hash 479 | ) 480 | .then((r) => { 481 | !r && success(); 482 | if (r.length) { 483 | chks = chunks!.filter((v) => !r.includes(v.chunkname)); 484 | r.forEach((key) => { 485 | context._progressGather[key] = 1; 486 | }); 487 | } 488 | count -= r.length; 489 | success(); 490 | }) 491 | .catch((err) => 492 | handleCanceledError(err, () => 493 | reject({ 494 | type: UPLOAD_ERROR_STATES.GET_CHUNKS_ERROR, 495 | reason: err?.message || err?.reason, 496 | err, 497 | }) 498 | ) 499 | ); 500 | } 501 | ); 502 | } 503 | } 504 | 505 | export const useUploader = (opt: Options) => { 506 | const instance = new UxChunkUploader(opt); 507 | let tAbort: null | (() => void) = null; 508 | let tSuccess: (() => void) | null = null; 509 | let tFail: ((e: ERROR_RESPOSE) => void) | null = null; 510 | let tFinally: (() => void) | null = null; 511 | const onSuccess = (t: () => void) => { 512 | tSuccess = t; 513 | }; 514 | const onFail = (t: (e: ERROR_RESPOSE) => void) => { 515 | tFail = t; 516 | }; 517 | const onFinally = (t: () => void) => { 518 | tFinally = t; 519 | }; 520 | 521 | const trigger = (file: RawFile, opt?: Partial) => { 522 | const { response, terminate } = instance.triggerUpload(file, opt); 523 | tAbort = terminate; 524 | return response 525 | .then(() => { 526 | typeof tSuccess === "function" && tSuccess(); 527 | return Promise.resolve(void 0); 528 | }) 529 | .catch((e: ERROR_RESPOSE) => { 530 | typeof tFail === "function" && tFail(e); 531 | return Promise.reject(e); 532 | }) 533 | .finally(() => { 534 | typeof tFinally === "function" && tFinally(); 535 | }); 536 | }; 537 | 538 | const abort = () => { 539 | if (tAbort) { 540 | tAbort(); 541 | tAbort = null; 542 | } 543 | }; 544 | 545 | return { 546 | trigger, 547 | abort, 548 | onSuccess, 549 | onFail, 550 | onFinally, 551 | }; 552 | }; 553 | -------------------------------------------------------------------------------- /src/packages/modules/handles/clearRuntimeProduct.ts: -------------------------------------------------------------------------------- 1 | import type {_SetOption} from '@/types'; 2 | 3 | export const clearRuntimeProduct = (setOption:_SetOption) => { 4 | setOption({ 5 | _chunkcount:0, 6 | _progressGather:{} 7 | }); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/packages/modules/handles/createChunks.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CreateChunks, 3 | Chunks, 4 | _SetOption 5 | } from '@/types'; 6 | import {callReject,ToM} from '@/shared'; 7 | import {UPLOAD_ERROR_STATES} from '@/constant'; 8 | 9 | export const createChunks = function({ raw, hash, suffix, chunkSize, max }: Parameters[0],isTerminate:()=>boolean,setOption:_SetOption) { 10 | const chunks: Chunks = []; 11 | let localChunkSize = ToM(chunkSize); 12 | let start = 0; 13 | let count = Math.ceil(raw.size / localChunkSize); 14 | count > max && (() => { 15 | count = max; 16 | localChunkSize = raw.size / count; 17 | })(); 18 | setOption({ 19 | _chunkcount:count, 20 | }); 21 | while (start < count) { 22 | if(isTerminate()) { 23 | const err = callReject({ 24 | type:UPLOAD_ERROR_STATES.TERMINATE_UPLOAD, 25 | reason:`${UPLOAD_ERROR_STATES.CREATE_CHUNK_ERROR}:terminated upload`, 26 | }); 27 | return { 28 | err, 29 | chunks:[] as Chunks 30 | } 31 | } 32 | const end = Math.min(raw.size, (start + 1) * localChunkSize); 33 | const chunkname = `${hash}_${start}.${suffix}`; 34 | setOption({ 35 | _progressGather:{ 36 | [chunkname]:0 37 | } 38 | }) 39 | chunks.push({ 40 | chunk: raw.slice(start * localChunkSize, end), 41 | chunkname, 42 | }); 43 | start++; 44 | }; 45 | return { 46 | err:void 0, 47 | chunks 48 | } 49 | }; -------------------------------------------------------------------------------- /src/packages/modules/handles/createHash/index.ts: -------------------------------------------------------------------------------- 1 | import type { CreateHash, UploadError } from "@/types"; 2 | // @ts-ignore 3 | import CreateHashWorker from "./worker/index?worker&inline"; 4 | import type { MesBody } from "./worker/types"; 5 | import type { Terminable } from '@/hooks/types'; 6 | import { genHash } from "./shared"; 7 | import { UPLOAD_ERROR_STATES } from '@/constant'; 8 | 9 | 10 | export const createHash = ( 11 | file: Parameters[0], 12 | addTerminable: Terminable["push"] 13 | ): Promise<{ 14 | hash: string; 15 | filename: string; 16 | suffix: string; 17 | }> => { 18 | const win = window; 19 | return new Promise( 20 | ( 21 | resolve: (value: { 22 | hash: string; 23 | suffix: string; 24 | filename: string; 25 | }) => void, 26 | reject: (reason: UploadError) => void 27 | ) => { 28 | const handleWorker = () => { 29 | const createHashWorker = new CreateHashWorker(); 30 | createHashWorker?.postMessage(file); 31 | const terminateWorker = () => { 32 | createHashWorker.terminate(); 33 | } 34 | addTerminable(() => { 35 | terminateWorker(); 36 | reject({ 37 | type:UPLOAD_ERROR_STATES.TERMINATE_UPLOAD, 38 | reason:`${UPLOAD_ERROR_STATES.CREATE_HASH_ERROR}:terminated upload` 39 | }) 40 | }); 41 | createHashWorker.onmessage = (e: MesBody) => { 42 | const { err, data } = e.data; 43 | if (err) { 44 | terminateWorker(); 45 | return reject(err); 46 | } 47 | resolve(data); 48 | }; 49 | 50 | createHashWorker.onerror = (err:ErrorEvent) => { 51 | terminateWorker(); 52 | reject({ 53 | type: UPLOAD_ERROR_STATES.CREATE_HASH_ERROR, 54 | err, 55 | reason: err.message, 56 | }); 57 | }; 58 | }; 59 | const handle = () => { 60 | const { data, abort } = genHash(file); 61 | addTerminable(abort); 62 | data 63 | .then((r) => resolve(r)) 64 | .catch(reject); 65 | }; 66 | win.Worker ? handleWorker() : handle(); 67 | } 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/packages/modules/handles/createHash/shared.ts: -------------------------------------------------------------------------------- 1 | import SparkMD5 from 'spark-md5'; 2 | import { findSuffix } from '@/shared/index'; 3 | import type { 4 | RawFile, 5 | ERROR_RESPOSE 6 | } from '@/types'; 7 | import type {TerminableTaskItem} from '@/hooks/types'; 8 | import { UPLOAD_ERROR_STATES } from '@/constant'; 9 | 10 | 11 | export const genHash = (file:RawFile) => { 12 | let abort:TerminableTaskItem|null = null; 13 | const p = new Promise((resolve:(value:{hash:string,suffix:string,filename:string})=>void,reject:(err: ERROR_RESPOSE)=>void)=>{ 14 | const spark:SparkMD5.ArrayBuffer|null = new SparkMD5.ArrayBuffer(); 15 | const filereander = new FileReader(); 16 | filereander.readAsArrayBuffer(file); 17 | const handleResove = (hash:string) => { 18 | const suffix = findSuffix(file.name)!; 19 | return { 20 | hash, 21 | suffix, 22 | filename: `${hash}.${suffix}` 23 | }; 24 | }; 25 | filereander.onload = function (this: InstanceType,e) { 26 | const result = e.target!.result as ArrayBuffer; 27 | spark!.append(result); 28 | const hash = spark!.end(); 29 | const value = handleResove(hash); 30 | resolve(value); 31 | }; 32 | filereander.onerror = (err) => { 33 | reject({ 34 | type:UPLOAD_ERROR_STATES.CREATE_HASH_ERROR, 35 | reason:'filereander.onerror', 36 | err, 37 | }); 38 | }; 39 | abort = () => { 40 | reject({ 41 | type:UPLOAD_ERROR_STATES.TERMINATE_UPLOAD, 42 | reason:`${UPLOAD_ERROR_STATES.CREATE_HASH_ERROR}:terminated upload` 43 | }); 44 | filereander.abort(); 45 | } 46 | }); 47 | 48 | return { 49 | data:p, 50 | abort:abort! 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/packages/modules/handles/createHash/worker/index.ts: -------------------------------------------------------------------------------- 1 | import type {MesEvent} from './types'; 2 | import {genHash} from '../shared'; 3 | 4 | self.addEventListener("message", (e:MesEvent) => { 5 | const {data} = genHash(e.data) 6 | data.then(r=>self.postMessage({err:void 0,data:r})) 7 | .catch((err)=>self.postMessage({err,data:null})); 8 | }); 9 | -------------------------------------------------------------------------------- /src/packages/modules/handles/createHash/worker/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RawFile, 3 | ERROR_RESPOSE 4 | } from '@/types'; 5 | export type MesEvent = MessageEvent; 6 | export type R = { hash: string; suffix: string; filename: string }; 7 | export type MesBody = MessageEvent<{ 8 | err:ERROR_RESPOSE; 9 | data:R; 10 | }> -------------------------------------------------------------------------------- /src/packages/modules/handles/index.ts: -------------------------------------------------------------------------------- 1 | import {createHash} from './createHash'; 2 | import {createChunks} from './createChunks'; 3 | import {clearRuntimeProduct} from './clearRuntimeProduct'; 4 | 5 | export { 6 | createHash, 7 | createChunks, 8 | clearRuntimeProduct 9 | } 10 | -------------------------------------------------------------------------------- /src/packages/shared/index.ts: -------------------------------------------------------------------------------- 1 | import {CanceledError} from 'axios'; 2 | import {UPLOAD_ERROR_STATES} from '../constant'; 3 | import type {ErrorType} from '@/types'; 4 | export const findSuffix = (filename: string) => { 5 | if(typeof filename !== 'string') { 6 | return void 0; 7 | } 8 | const lastIndex = filename.lastIndexOf('.'); 9 | return filename.substring(lastIndex + 1); 10 | }; 11 | 12 | export const headers = { 13 | 'Content-Type': 'multipart/form-data' 14 | }; 15 | 16 | 17 | /** 18 | * Loop through the object 19 | * 20 | * @template T 21 | * @param {T} obj 22 | * @param {(v: T[keyof T], k: keyof T) => void} cb 23 | * @returns {void) => void} 24 | */ 25 | export const objFor = (obj: T, cb: (v: T[keyof T], k: keyof T) => void) => { 26 | const has = Object.prototype.hasOwnProperty; 27 | for (const key in obj) { 28 | if (has.call(obj, key)) { 29 | cb(obj[key], key); 30 | } 31 | } 32 | }; 33 | 34 | /** 35 | * Check if it is of type Object 36 | * 37 | * @template T 38 | * @param {T} target 39 | * @returns {boolean} 40 | */ 41 | export const isObject = (target: T) => Object.prototype.toString.call(target) === '[object Object]'; 42 | 43 | export const mergeOptions = (target: T, source: T) => { 44 | if(!isObject(source)) return target as Required; 45 | if(!target) { 46 | return void 0; 47 | } 48 | objFor(source, 49 | (v, k) => isObject(v) ? 50 | (mergeOptions(target, v as T)) 51 | : ( v === null || v === void 0 ? (null) : 52 | (target[k] = v as NonNullable[keyof T])) 53 | ) 54 | return target as Required; 55 | } 56 | 57 | export const ToM = (n:number) => 1024*1024*n; 58 | 59 | 60 | /** 61 | * Normalize error format 62 | * 63 | * @param {{ type: ErrorType, reason: string, err?: Error }} param0 64 | * @param {ErrorType} param0.type 65 | * @param {string} param0.reason 66 | * @param {Error} param0.err 67 | * @param {?()=>void} [callback] 68 | * @returns {void) => any} 69 | */ 70 | export const callReject = ({ type, reason, err }: { type: ErrorType, reason: string, err?: Error },callback?:()=>void) => { 71 | if(callback) callback(); 72 | return Promise.reject({ 73 | type, 74 | reason, 75 | err 76 | }); 77 | }; 78 | 79 | 80 | 81 | /** 82 | * Handle axios request cancellation errors 83 | * 84 | * @template T 85 | * @param {T} err 86 | * @param {()=>void} callback 87 | * @returns {void) => any} 88 | */ 89 | export const handleCanceledError = (err: T,callback:()=>void) => { 90 | if (err instanceof CanceledError) 91 | return callReject({ 92 | type: UPLOAD_ERROR_STATES.TERMINATE_UPLOAD, 93 | err, 94 | reason: err.message, 95 | }); 96 | return callback(); 97 | }; 98 | 99 | type callTypeofResponse = 'object'|'array'|'string'|'boolean'|'function'|'null'|'regexp'|'bigint'|'symbol'|undefined; 100 | export function callTypeof(target:T):callTypeofResponse { 101 | const map = new Map([ 102 | ['[object Object]', 'object'], 103 | ['[object Array]', 'array'], 104 | ['[object String]', 'string'], 105 | ['[object Number]', 'number'], 106 | ['[object Boolean]', 'boolean'], 107 | ['[object Function]', 'function'], 108 | ['[object Null]', 'null'], 109 | ['[object Undefined]', 'undefined'], 110 | ['[object Date]', 'date'], 111 | ['[object RegExp]', 'regexp'], 112 | ['[object Bigint]', 'bigint'], 113 | ['[object Symbol]', 'symbol'], 114 | ]); 115 | const typeStr = Object.prototype.toString.call(target); 116 | return map.get(typeStr) as callTypeofResponse; 117 | } 118 | 119 | type AnyFunction = (...args: any[]) => any; 120 | 121 | /** 122 | * 123 | * @template {AnyFunction} T 124 | * @param {T} f 125 | * @param {?T} [g] 126 | * @returns {T} 127 | */ 128 | export const isExistF = (f:T,g?:T) =>{ 129 | if(g) { 130 | return g; 131 | } return f; 132 | }; 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/packages/types.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { AxiosInstance, AxiosResponse } from 'axios'; 3 | import type {UseNextTick} from './hooks/types'; 4 | import { useControl } from './hooks/useControl'; 5 | 6 | export type Control = ReturnType; 7 | 8 | export type ErrorType = 'get-chunks-error' | 'upload-chunk-error' | 'merge-chunks-error' | 'create-hash-error' | 'file-error'|'terminate-upload'|'create-chunk-error'; 9 | 10 | export type ERROR_RESPOSE = { 11 | type: ErrorType; 12 | reason: string; 13 | err?: T; 14 | }; 15 | 16 | 17 | export type RawFile = File; 18 | export type requestConfig = { 19 | instance: T; 20 | } 21 | 22 | export interface OnReponse { 23 | (response: AxiosResponse): U; 24 | } 25 | 26 | export type DeepPartial = { 27 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 28 | }; 29 | 30 | export type DeepRequired = { 31 | [P in keyof T]?: T[P] extends object ? DeepRequired : T[P]; 32 | }; 33 | 34 | 35 | 36 | 37 | export type UploadError = ERROR_RESPOSE; 38 | 39 | export type HasNull = T | null; 40 | export type Chunks = Array<{ 41 | chunk: Blob; 42 | chunkname: string; 43 | }>; 44 | 45 | 46 | 47 | export type UploadChunkSetParams = Record & { 48 | dirname:string; 49 | chunkname:string; 50 | chunk:Blob; 51 | }; 52 | // Uploading Slices, and Merging Slices 53 | export type RequestFunc = { 54 | getChunk: (url: string, hash: string) => Promise; 55 | uploadChunk: (opt: { formData: FormData, url: string,taskIndex:number }) => Promise; 56 | merge: (url: string, opt: { hash: string; chunkcount: number; filename: string } ) => Promise; 57 | } 58 | 59 | export type UploadMergeSetParams = Parameters[1] & Record; 60 | 61 | export type SetParamsAssemble = { 62 | uploadChunk:(chunkInfo:UploadChunkSetParams)=> Record; 63 | merge:(mergeParams:UploadMergeSetParams)=>UploadMergeSetParams; 64 | getChunks:(hash:string)=>Record; 65 | } 66 | 67 | 68 | export type RequestUploadChunk = { 69 | (r:AxiosResponse):number; 70 | } 71 | export type RequestMerge = { 72 | (r:AxiosResponse):number; 73 | } 74 | export type RequestGetChunks = { 75 | (r:AxiosResponse):{ 76 | code:200, 77 | data:string[] 78 | }; 79 | } 80 | 81 | export type OptionsRequestConfig = { 82 | instance?: AxiosInstance; 83 | uploadChunk: { 84 | url: string; 85 | onResponse?: RequestUploadChunk; 86 | setParams?:SetParamsAssemble['uploadChunk']; 87 | }; 88 | merge: { 89 | url: string; 90 | onResponse?: RequestMerge; 91 | setParams?:SetParamsAssemble['merge']; 92 | } 93 | getChunks?: { 94 | url: string; 95 | onResponse?: RequestGetChunks; 96 | setParams?:SetParamsAssemble['getChunks']; 97 | } 98 | } 99 | 100 | 101 | 102 | export interface OnProgress { 103 | (v: number): void; 104 | } 105 | 106 | 107 | export type UploaderConfig = { 108 | chunkSize: number; 109 | limit: number; 110 | max: number; 111 | onProgress: OnProgress; 112 | } 113 | 114 | export type Options = { 115 | requestConfig: OptionsRequestConfig; 116 | uploadConfig?: Partial; 117 | } 118 | 119 | export interface CreateChunks { 120 | (options: { raw: RawFile; hash: string; suffix: string; chunkSize: number; max: number; }): { 121 | chunks:Chunks; 122 | is:boolean; 123 | } 124 | } 125 | 126 | export interface CreateHash { 127 | (raw: RawFile): Promise<{ 128 | hash: string; 129 | suffix: string; 130 | filename: string; 131 | }> 132 | } 133 | 134 | type ChunkBasic = { 135 | chunks: Chunks; 136 | hash: string; 137 | filename:string 138 | } 139 | 140 | export interface UploadChunks { 141 | (options: ChunkBasic & { limit: number;onProgress?: OnProgress;count:number; }): Promise; 142 | } 143 | 144 | export interface _GetChunks { 145 | (options: ChunkBasic): Promise; 146 | } 147 | 148 | export interface TriggerUpload { 149 | (file: RawFile, opt?: Partial): { 150 | terminate: () => void; 151 | response: Promise; 152 | }; 153 | } 154 | 155 | 156 | export type ChunkUploaderProps = { 157 | options: Partial | null; 158 | } 159 | 160 | export type PrivateChunkUploaderProps = { 161 | chunkcount: number; 162 | progressGather: Record; 163 | requestFunc: RequestFunc | null; 164 | control: Control; 165 | } 166 | 167 | export type ChunkUploaderMethods = { 168 | triggerUpload: TriggerUpload; 169 | setOptions:(options: Partial) =>void; 170 | } 171 | export interface UxChunkUploaderTools { 172 | $nextTick:ReturnType; 173 | } 174 | 175 | export interface ChunkUploader extends ChunkUploaderProps,ChunkUploaderMethods,UxChunkUploaderTools {}; 176 | 177 | 178 | 179 | export type _SetOption = (options:Record)=>void; 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YTXEternal/ux-plus-chunk-uploader/5c2a4ce452f67bd62b2dba4989da57e030a86296/src/style.css -------------------------------------------------------------------------------- /src/upload.ts: -------------------------------------------------------------------------------- 1 | import type {ERROR_RESPOSE} from './packages/types'; 2 | import {useUploader} from './packages'; 3 | 4 | export const setupUploader = () => { 5 | const uploader = document.querySelector('#uploader')! as HTMLInputElement; 6 | const {onFail, onFinally, onSuccess, abort, trigger} = useUploader({ 7 | // Request configuration 8 | requestConfig: { 9 | // API for retrieving cached slices 10 | getChunks: { 11 | url: '/api/chunks', 12 | /** 13 | * Intercept Response 14 | * 15 | * @param {AxiosResponse} r 16 | * @returns {{code:number;data:string[]}} 17 | */ 18 | onResponse(r) { 19 | return r.data; 20 | }, 21 | /** 22 | * Set request parameters 23 | * @param {string} hash 24 | * @returns {Record} 25 | */ 26 | setParams(hash) { 27 | return { 28 | hash 29 | }; 30 | } 31 | }, 32 | // API for uploading slices 33 | uploadChunk: { 34 | url: '/api/uploadchunk', 35 | /** 36 | * Intercept Response 37 | * 38 | * @param {AxiosResponse} r 39 | * @returns {number} 40 | */ 41 | onResponse(r) { 42 | return r.data.code; 43 | }, 44 | /** 45 | * Set request parameters 46 | * 47 | * @param {{ 48 | * dirname: string; 49 | * chunkname: string; 50 | * chunk: Blob; 51 | * }} r 52 | * @returns {Record} 53 | */ 54 | setParams(r) { 55 | return r; 56 | } 57 | }, 58 | // API for merging slices 59 | merge: { 60 | url: '/api/mergechunks', 61 | /** 62 | * Intercept Response 63 | * 64 | * @param {AxiosResponse} r 65 | * @returns {number} 66 | */ 67 | onResponse(r) { 68 | return r.data.code; 69 | }, 70 | /** 71 | * Set request parameters 72 | * 73 | * @param {{ hash: string; chunkcount: number; filename: string }} r 74 | * @returns {Record} 75 | */ 76 | setParams(r) { 77 | return r; 78 | } 79 | }, 80 | }, 81 | // Global file upload configuration 82 | uploadConfig: { 83 | // Size of each slice in MB 84 | chunkSize: 100, 85 | // Number of concurrent slices to upload 86 | limit: 5, 87 | // Maximum number of slices 88 | max: 100, 89 | }, 90 | }); 91 | 92 | const terminateBtn = document.querySelector('#terminate')! as HTMLInputElement; 93 | uploader.onchange = async (e: Event) => { 94 | const target = e.target as HTMLInputElement; 95 | const file = target.files![0]; 96 | trigger(file, 97 | // The configuration here will override the global file upload configuration. 98 | { 99 | /** 100 | * Upload Progress Listener 101 | * 102 | * @param {number} v 103 | */ 104 | onProgress(v) { 105 | console.log('progress', v); 106 | }, 107 | limit: 5, 108 | max: 100, 109 | }); 110 | terminateBtn.onclick = () => { 111 | // Cancel upload 112 | abort(); 113 | }; 114 | // Called when upload and slice merging succeed 115 | onSuccess(() => { 116 | console.log("Upload succeeded"); 117 | }); 118 | // Called if the upload or slice merging fails (including upload cancellation) 119 | onFail((e: ERROR_RESPOSE) => { 120 | console.log("Upload failed", e); 121 | }); 122 | // Called once the upload and slice merging request completes (including upload cancellation) 123 | onFinally(() => { 124 | console.log("Upload completed"); 125 | }); 126 | }; 127 | }; -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tests/client/fileUpload.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi, test } from "vitest"; 2 | import { useUploader, type ERROR_RESPOSE, Options } from "@/index"; 3 | import { instanceNormal } from "./mock"; 4 | const createLargeFile = (sizeInBytes: number) => { 5 | // 创建一个指定大小的 ArrayBuffer 6 | const arrayBuffer = new ArrayBuffer(sizeInBytes); 7 | // 创建一个视图以便我们可以填充数据 8 | const view = new Uint8Array(arrayBuffer); 9 | // 填充一些示例数据 10 | for (let i = 0; i < sizeInBytes; ++i) { 11 | view[i] = i % 256; 12 | } 13 | // 使用这个 ArrayBuffer 创建一个 Blob 或 File 对象 14 | return new File([view], "largefile.dat", { 15 | type: "application/octet-stream", 16 | }); 17 | }; 18 | 19 | const config: Options = { 20 | // 请求配置 21 | requestConfig: { 22 | // 获取已缓存切片接口 23 | getChunks: { 24 | url: "/api/chunks", 25 | onResponse(r) { 26 | return r.data; 27 | }, 28 | setParams(hash) { 29 | return { 30 | hash, 31 | }; 32 | }, 33 | }, 34 | // 上传切片接口 35 | uploadChunk: { 36 | url: "/api/uploadchunk", 37 | onResponse(r) { 38 | return r.data.code; 39 | }, 40 | setParams(r) { 41 | return r; 42 | }, 43 | }, 44 | // 合并切片接口 45 | merge: { 46 | url: "/api/mergechunks", 47 | onResponse(r) { 48 | return r.data.code; 49 | }, 50 | setParams(r) { 51 | return r; 52 | }, 53 | }, 54 | instance: instanceNormal, 55 | }, 56 | // 全局上传文件配置 57 | uploadConfig: { 58 | // 每个切片大小(MB) 59 | chunkSize: 100, 60 | // 并发上传切片数 61 | limit: 5, 62 | // 最大分片数 63 | max: 100, 64 | }, 65 | }; 66 | // retain it 67 | // const useRevertConfigUrl = (opt:{get:string;upload:string;merge:string},instance?:Options['requestConfig']['instance']) => { 68 | // const option:Options = { 69 | // ...config, 70 | // requestConfig:{ 71 | // getChunks:{...config.requestConfig.getChunks} as Options['requestConfig']['getChunks'], 72 | // uploadChunk:{...config.requestConfig.uploadChunk}, 73 | // merge:{...config.requestConfig.merge}, 74 | // instance:instanceNormal 75 | // } 76 | // }; 77 | // option.requestConfig.getChunks!.url = opt.get; 78 | // option.requestConfig.uploadChunk.url = opt.upload; 79 | // option.requestConfig.merge.url = opt.merge; 80 | // if(instance) { 81 | // option.requestConfig.instance = instance; 82 | // } 83 | // return option; 84 | // }; 85 | 86 | const file = createLargeFile(125036); 87 | 88 | describe("useUploader", () => { 89 | it("onSuccess", async () => { 90 | const { trigger, onSuccess } = useUploader(config); 91 | const mockFn = vi.fn(); 92 | onSuccess(mockFn); 93 | await trigger(file); 94 | expect(mockFn).toHaveBeenCalled(); 95 | }); 96 | it("onFinally", async () => { 97 | const { trigger, onFinally } = useUploader(config); 98 | const mockFn = vi.fn(); 99 | onFinally(mockFn); 100 | await trigger(file); 101 | expect(mockFn).toHaveBeenCalled(); 102 | }); 103 | it("onProgress", async () => { 104 | const { trigger } = useUploader(config); 105 | const mockProgress = vi.fn(); 106 | await trigger(file, { 107 | onProgress: mockProgress, 108 | }); 109 | expect(mockProgress).toHaveBeenCalled(); 110 | }); 111 | it("abort", async () => { 112 | const { trigger, abort } = useUploader(config); 113 | try { 114 | await trigger(file, { 115 | onProgress() { 116 | abort(); 117 | }, 118 | }); 119 | } catch (e) { 120 | const err = e as ERROR_RESPOSE; 121 | expect(err.type).toBe("terminate-upload"); 122 | } 123 | }); 124 | 125 | test("All of it", async () => { 126 | try { 127 | const { trigger, onFinally, onSuccess } = useUploader(config); 128 | const mockSuccess = vi.fn(); 129 | const mockOnProgress = vi.fn(); 130 | const mockFinally = vi.fn(); 131 | onSuccess(mockSuccess); 132 | onFinally(mockFinally); 133 | await trigger(file, { 134 | onProgress: mockOnProgress, 135 | }); 136 | expect(mockSuccess).toHaveBeenCalled(); 137 | expect(mockFinally).toHaveBeenCalled(); 138 | expect(mockOnProgress).toHaveBeenCalled(); 139 | } catch (e) { 140 | const err = e as ERROR_RESPOSE; 141 | expect(err.reason).toBe("This is an error"); 142 | } 143 | }); 144 | }); 145 | 146 | describe("Wrong boundary", () => { 147 | it("file is not a valid value", async () => { 148 | const { trigger, onFail } = useUploader(config); 149 | const mockFail = vi.fn(); 150 | try { 151 | onFail(mockFail); 152 | // @ts-ignore 153 | const r = await trigger(void 0); 154 | expect(r).toBe(void 0); 155 | } catch (e) { 156 | const err = e as ERROR_RESPOSE; 157 | expect(err.reason).toBe("file is not a valid value"); 158 | expect(mockFail).toHaveBeenCalled(); 159 | } 160 | }); 161 | it("Environment without Web Workers", async () => { 162 | const Worker = window.Worker; 163 | // @ts-ignore 164 | window.Worker = null; 165 | try { 166 | const { trigger, onSuccess } = useUploader(config); 167 | const mockSuccess = vi.fn(); 168 | onSuccess(mockSuccess); 169 | await trigger(file); 170 | expect(mockSuccess).toHaveBeenCalled(); 171 | } catch (e) { 172 | const err = e as ERROR_RESPOSE; 173 | expect(err.reason).toBe("This is an error"); 174 | } 175 | window.Worker = Worker; 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /tests/client/mock.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import MockAdapter from "axios-mock-adapter"; 3 | 4 | export const instanceNormal = axios.create(); 5 | export const instanceDelay = axios.create(); 6 | const mock = new MockAdapter(instanceNormal); 7 | const mockDelay = new MockAdapter(instanceDelay, { 8 | delayResponse: 20000, 9 | }); 10 | 11 | // 成功的模拟响应 12 | const defineSuccess = () => { 13 | // GET 请求匹配 /api/chunks? 开头的任何路径 14 | mock.onGet(/\/api\/chunks/).reply(() => [ 15 | 200, 16 | { 17 | code: 200, 18 | data: [], 19 | message: "", 20 | }, 21 | ]); 22 | 23 | // POST 请求到 /api/uploadchunk 24 | mock.onPost("/api/uploadchunk").reply(200, { 25 | code: 200, 26 | data: [], 27 | message: "", 28 | }); 29 | 30 | // POST 请求到 /api/mergechunks 31 | mock.onPost("/api/mergechunks").reply(200, { 32 | code: 200, 33 | data: [], 34 | message: "", 35 | }); 36 | }; 37 | 38 | // 失败的模拟响应 39 | const defineFail = () => { 40 | // GET 请求匹配 /api/chunks/fail? 开头的任何路径 41 | mock.onGet(/\/api\/chunks\/fail/).reply(200, { 42 | code: 400, 43 | data: [], 44 | message: "", 45 | }); 46 | 47 | // POST 请求到 /api/uploadchunk/fail 48 | mock.onPost("/api/uploadchunk/fail").reply(200, { 49 | code: 400, 50 | message: "", 51 | }); 52 | 53 | // POST 请求到 /api/mergechunks/fail 54 | mock.onPost("/api/mergechunks/fail").reply(200, { 55 | code: 400, 56 | message: "", 57 | }); 58 | }; 59 | 60 | // 延迟响应 61 | const defineLongTime = () => { 62 | // GET 请求匹配 /api/chunks/longtime? 开头的任何路径,并延迟2秒响应 63 | mockDelay.onGet("/api/chunks/longtime").reply( 64 | 200, 65 | { 66 | code: 200, 67 | data: [], 68 | }, 69 | { 70 | mockDelay: 2000, 71 | } 72 | ); 73 | 74 | // POST 请求到 /api/uploadchunk/longtime 并延迟2秒响应 75 | mock.onPost("/api/uploadchunk/longtime").reply(200, { 76 | code: 200, 77 | data: [], 78 | }); 79 | 80 | // POST 请求到 /api/mergechunks/longtime 并延迟2秒响应 81 | mock.onPost("/api/mergechunks/longtime").reply(200, { 82 | code: 200, 83 | data: [], 84 | }); 85 | }; 86 | 87 | defineSuccess(); 88 | defineFail(); 89 | defineLongTime(); 90 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/jsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "baseUrl": "./", 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "Bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "declaration": true, 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "paths": { 25 | "@/*":["src/packages/*"], 26 | } 27 | }, 28 | "include": ["src/packages"], 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "baseUrl": "./", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "Bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true, 23 | "paths": { 24 | "@/*":["src/packages/*"] 25 | } 26 | }, 27 | "include": ["src","tests"] 28 | } 29 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from "vitest/config"; 2 | import viteConfig from "./build/test"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | export default mergeConfig( 5 | viteConfig, 6 | defineConfig({ 7 | test: { 8 | workspace: "./vitest.workspace.ts", 9 | coverage: { 10 | provider: "v8", 11 | enabled: true, 12 | reporter: ["text", "json-summary", "json"], 13 | reportOnFailure: true, 14 | }, 15 | }, 16 | plugins: [tsconfigPaths()], 17 | }) 18 | ); 19 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from "vitest/config"; 2 | const url = new URL("./src/packages", import.meta.url).pathname; 3 | export default defineWorkspace([ 4 | { 5 | test: { 6 | exclude: ["node_modules", "dist"], 7 | name: "client", 8 | root: "tests/client", 9 | environment: "happy-dom", 10 | alias: { 11 | "@": url, 12 | }, 13 | testTimeout: 10 * 1000, 14 | }, 15 | }, 16 | ]); 17 | --------------------------------------------------------------------------------